@jbarket/loomai 0.5.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/CHANGELOG.md +475 -0
- package/LICENSE +661 -0
- package/README.md +702 -0
- package/assets/demo.cast +396 -0
- package/assets/skill/SKILL.md +192 -0
- package/dist/backends/embedding-runtime.d.ts +39 -0
- package/dist/backends/embedding-runtime.js +265 -0
- package/dist/backends/episodes.d.ts +36 -0
- package/dist/backends/episodes.js +126 -0
- package/dist/backends/fastembed.d.ts +18 -0
- package/dist/backends/fastembed.js +95 -0
- package/dist/backends/glob.d.ts +10 -0
- package/dist/backends/glob.js +16 -0
- package/dist/backends/index.d.ts +6 -0
- package/dist/backends/index.js +78 -0
- package/dist/backends/migrations.d.ts +38 -0
- package/dist/backends/migrations.js +157 -0
- package/dist/backends/mmr.d.ts +45 -0
- package/dist/backends/mmr.js +82 -0
- package/dist/backends/proposals.d.ts +65 -0
- package/dist/backends/proposals.js +182 -0
- package/dist/backends/recall-log.d.ts +42 -0
- package/dist/backends/recall-log.js +143 -0
- package/dist/backends/retry.d.ts +29 -0
- package/dist/backends/retry.js +60 -0
- package/dist/backends/salience.d.ts +57 -0
- package/dist/backends/salience.js +186 -0
- package/dist/backends/sqlite-knowledge.d.ts +37 -0
- package/dist/backends/sqlite-knowledge.js +788 -0
- package/dist/backends/sqlite-vec.d.ts +64 -0
- package/dist/backends/sqlite-vec.js +737 -0
- package/dist/backends/ttl.d.ts +24 -0
- package/dist/backends/ttl.js +50 -0
- package/dist/backends/types.d.ts +562 -0
- package/dist/backends/types.js +8 -0
- package/dist/blocks/harness.d.ts +45 -0
- package/dist/blocks/harness.js +142 -0
- package/dist/blocks/model.d.ts +4 -0
- package/dist/blocks/model.js +56 -0
- package/dist/blocks/types.d.ts +40 -0
- package/dist/blocks/types.js +34 -0
- package/dist/categories.d.ts +16 -0
- package/dist/categories.js +29 -0
- package/dist/cli/args.d.ts +21 -0
- package/dist/cli/args.js +47 -0
- package/dist/cli/bootstrap.d.ts +2 -0
- package/dist/cli/bootstrap.js +214 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +206 -0
- package/dist/cli/forget.d.ts +2 -0
- package/dist/cli/forget.js +97 -0
- package/dist/cli/harness.d.ts +2 -0
- package/dist/cli/harness.js +91 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/inject.d.ts +2 -0
- package/dist/cli/inject.js +298 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +137 -0
- package/dist/cli/io.d.ts +14 -0
- package/dist/cli/io.js +63 -0
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +216 -0
- package/dist/cli/memory.d.ts +2 -0
- package/dist/cli/memory.js +608 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.js +116 -0
- package/dist/cli/recall.d.ts +2 -0
- package/dist/cli/recall.js +91 -0
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +111 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.js +71 -0
- package/dist/cli/subcommands.d.ts +7 -0
- package/dist/cli/subcommands.js +11 -0
- package/dist/cli/tui/multi-select.d.ts +53 -0
- package/dist/cli/tui/multi-select.js +116 -0
- package/dist/cli/update-identity.d.ts +2 -0
- package/dist/cli/update-identity.js +90 -0
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.js +87 -0
- package/dist/cli/wake.d.ts +2 -0
- package/dist/cli/wake.js +53 -0
- package/dist/clients.d.ts +8 -0
- package/dist/clients.js +63 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +160 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +75 -0
- package/dist/injection/harnesses.d.ts +17 -0
- package/dist/injection/harnesses.js +51 -0
- package/dist/injection/render.d.ts +17 -0
- package/dist/injection/render.js +50 -0
- package/dist/injection/writer.d.ts +25 -0
- package/dist/injection/writer.js +118 -0
- package/dist/install/harnesses.d.ts +26 -0
- package/dist/install/harnesses.js +90 -0
- package/dist/install/names.d.ts +16 -0
- package/dist/install/names.js +36 -0
- package/dist/install/render.d.ts +18 -0
- package/dist/install/render.js +42 -0
- package/dist/install/skill-source.d.ts +1 -0
- package/dist/install/skill-source.js +11 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +62 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +571 -0
- package/dist/tools/append-maintenance-log.d.ts +52 -0
- package/dist/tools/append-maintenance-log.js +103 -0
- package/dist/tools/archive.d.ts +2 -0
- package/dist/tools/archive.js +21 -0
- package/dist/tools/bootstrap.d.ts +11 -0
- package/dist/tools/bootstrap.js +161 -0
- package/dist/tools/dossier.d.ts +1 -0
- package/dist/tools/dossier.js +102 -0
- package/dist/tools/eurorack-import.d.ts +47 -0
- package/dist/tools/eurorack-import.js +199 -0
- package/dist/tools/find-similar.d.ts +3 -0
- package/dist/tools/find-similar.js +26 -0
- package/dist/tools/forget.d.ts +6 -0
- package/dist/tools/forget.js +70 -0
- package/dist/tools/harness.d.ts +18 -0
- package/dist/tools/harness.js +54 -0
- package/dist/tools/identity-scaffold.d.ts +22 -0
- package/dist/tools/identity-scaffold.js +142 -0
- package/dist/tools/identity.d.ts +4 -0
- package/dist/tools/identity.js +226 -0
- package/dist/tools/knowledge-archive.d.ts +5 -0
- package/dist/tools/knowledge-archive.js +24 -0
- package/dist/tools/knowledge-history.d.ts +6 -0
- package/dist/tools/knowledge-history.js +55 -0
- package/dist/tools/knowledge-maintain.d.ts +9 -0
- package/dist/tools/knowledge-maintain.js +113 -0
- package/dist/tools/knowledge-merge.d.ts +8 -0
- package/dist/tools/knowledge-merge.js +40 -0
- package/dist/tools/knowledge-move.d.ts +10 -0
- package/dist/tools/knowledge-move.js +48 -0
- package/dist/tools/knowledge-purge.d.ts +5 -0
- package/dist/tools/knowledge-purge.js +20 -0
- package/dist/tools/knowledge-recall.d.ts +20 -0
- package/dist/tools/knowledge-recall.js +142 -0
- package/dist/tools/knowledge-restore.d.ts +4 -0
- package/dist/tools/knowledge-restore.js +24 -0
- package/dist/tools/knowledge-supersede.d.ts +6 -0
- package/dist/tools/knowledge-supersede.js +36 -0
- package/dist/tools/knowledge-verify.d.ts +8 -0
- package/dist/tools/knowledge-verify.js +39 -0
- package/dist/tools/knowledge-write.d.ts +25 -0
- package/dist/tools/knowledge-write.js +87 -0
- package/dist/tools/memory-audit.d.ts +3 -0
- package/dist/tools/memory-audit.js +47 -0
- package/dist/tools/memory-list.d.ts +4 -0
- package/dist/tools/memory-list.js +23 -0
- package/dist/tools/prune.d.ts +4 -0
- package/dist/tools/prune.js +28 -0
- package/dist/tools/recall.d.ts +4 -0
- package/dist/tools/recall.js +24 -0
- package/dist/tools/remember.d.ts +10 -0
- package/dist/tools/remember.js +43 -0
- package/dist/tools/restore.d.ts +2 -0
- package/dist/tools/restore.js +20 -0
- package/dist/tools/update-identity.d.ts +34 -0
- package/dist/tools/update-identity.js +194 -0
- package/dist/tools/update.d.ts +2 -0
- package/dist/tools/update.js +16 -0
- package/dist/transport/guards.d.ts +35 -0
- package/dist/transport/guards.js +107 -0
- package/dist/transport/http-server.d.ts +17 -0
- package/dist/transport/http-server.js +198 -0
- package/dist/transport/mcp-app-hello.d.ts +3 -0
- package/dist/transport/mcp-app-hello.js +66 -0
- package/dist/widgets/display-modes.html +131 -0
- package/dist/widgets/hello.html +138 -0
- package/dist/widgets/proposals.html +131 -0
- package/dist/widgets/top-of-mind.html +131 -0
- package/package.json +73 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface Tier2Entry {
|
|
2
|
+
/** Defaults to "Art-Consolidate autopilot". */
|
|
3
|
+
runName?: string;
|
|
4
|
+
memoriesAudited: number;
|
|
5
|
+
staleCount: number;
|
|
6
|
+
duplicateCount: number;
|
|
7
|
+
expiredCount: number;
|
|
8
|
+
mergeCount: number;
|
|
9
|
+
contradictions: number;
|
|
10
|
+
pruned: number;
|
|
11
|
+
notes?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Tier3Entry {
|
|
14
|
+
/** Defaults to "Art-Identity-Review routine". */
|
|
15
|
+
runName?: string;
|
|
16
|
+
/** Section names updated in preferences.md (empty/omitted = no changes). */
|
|
17
|
+
preferencesUpdated?: string[];
|
|
18
|
+
/** Section names updated in self-model.md (empty/omitted = no changes). */
|
|
19
|
+
selfModelUpdated?: string[];
|
|
20
|
+
/** Whether IDENTITY.md itself changed. */
|
|
21
|
+
identityChanged?: boolean;
|
|
22
|
+
projectBriefsAccessed: number;
|
|
23
|
+
projectBriefsStale: number;
|
|
24
|
+
/** Free-form harness-manifest status, e.g. "4/4 present and current". */
|
|
25
|
+
harnessManifests?: string;
|
|
26
|
+
notes?: string;
|
|
27
|
+
}
|
|
28
|
+
export type MaintenanceLogEntry = {
|
|
29
|
+
type: 'tier-2';
|
|
30
|
+
timestamp?: Date;
|
|
31
|
+
tier2: Tier2Entry;
|
|
32
|
+
tier3?: never;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'tier-3';
|
|
35
|
+
timestamp?: Date;
|
|
36
|
+
tier3: Tier3Entry;
|
|
37
|
+
tier2?: never;
|
|
38
|
+
};
|
|
39
|
+
export interface AppendResult {
|
|
40
|
+
success: boolean;
|
|
41
|
+
filePath: string;
|
|
42
|
+
/** ISO date (YYYY-MM-DD) of the entry that was written. */
|
|
43
|
+
timestamp: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function formatEntry(entry: MaintenanceLogEntry): string;
|
|
46
|
+
export declare const FILE_HEADER: string;
|
|
47
|
+
/**
|
|
48
|
+
* Append a maintenance entry to `logFilePath`. Creates the file (with header) and
|
|
49
|
+
* any missing parent directories on first write. Each entry is separated from the
|
|
50
|
+
* previous content by a `---` thematic break.
|
|
51
|
+
*/
|
|
52
|
+
export declare function appendMaintenanceLog(entry: MaintenanceLogEntry, logFilePath: string): Promise<AppendResult>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* append-maintenance-log — appends a dated Markdown entry to a repo-tracked
|
|
3
|
+
* changelog file for memory-maintenance operations.
|
|
4
|
+
*
|
|
5
|
+
* Blessed decision 3 (SLE-89 / SLE-71): the consolidation digest goes to a file
|
|
6
|
+
* in the repo — not a `reference` memory, not an issue comment. This helper is the
|
|
7
|
+
* canonical formatter for that file, shared by both maintenance tracks:
|
|
8
|
+
* - Tier 2: weekly consolidation (Art-Consolidate)
|
|
9
|
+
* - Tier 3: monthly identity review (Art-Identity-Review)
|
|
10
|
+
*
|
|
11
|
+
* It is a plain filesystem append — NO git operations. Entries are append-only;
|
|
12
|
+
* the maintainer batches commits (agents do not commit). See docs/memory-maintenance.md.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
// ─── Formatting ───────────────────────────────────────────────────────────────
|
|
17
|
+
/** ISO date (YYYY-MM-DD) in UTC. Stable regardless of the host time zone. */
|
|
18
|
+
function isoDate(ts) {
|
|
19
|
+
return ts.toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
function formatTier2(date, data) {
|
|
22
|
+
const lines = [
|
|
23
|
+
`## [${date}] Weekly Consolidation (Tier 2)`,
|
|
24
|
+
'',
|
|
25
|
+
`- **Run:** ${data.runName ?? 'Art-Consolidate autopilot'}`,
|
|
26
|
+
`- **Memories audited:** ${data.memoriesAudited} ` +
|
|
27
|
+
`(${data.staleCount} stale, ${data.duplicateCount} duplicates, ${data.expiredCount} expired)`,
|
|
28
|
+
`- **Operations:**`,
|
|
29
|
+
` - Merges: ${data.mergeCount}`,
|
|
30
|
+
` - Contradictions: ${data.contradictions}`,
|
|
31
|
+
` - Pruned: ${data.pruned}`,
|
|
32
|
+
];
|
|
33
|
+
if (data.notes)
|
|
34
|
+
lines.push(`- **Notes:** ${data.notes}`);
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
function formatTier3(date, data) {
|
|
38
|
+
const prefs = data.preferencesUpdated ?? [];
|
|
39
|
+
const self = data.selfModelUpdated ?? [];
|
|
40
|
+
const lines = [
|
|
41
|
+
`## [${date}] Monthly Identity Review (Tier 3)`,
|
|
42
|
+
'',
|
|
43
|
+
`- **Run:** ${data.runName ?? 'Art-Identity-Review routine'}`,
|
|
44
|
+
`- **Manifest changes:**`,
|
|
45
|
+
prefs.length > 0
|
|
46
|
+
? ` - preferences.md: ${prefs.length} section(s) updated (${prefs.join(', ')})`
|
|
47
|
+
: ` - preferences.md: No changes`,
|
|
48
|
+
self.length > 0
|
|
49
|
+
? ` - self-model.md: ${self.length} section(s) updated (${self.join(', ')})`
|
|
50
|
+
: ` - self-model.md: No changes`,
|
|
51
|
+
` - IDENTITY.md: ${data.identityChanged ? 'Changes made' : 'No changes'}`,
|
|
52
|
+
`- **Project briefs:** ${data.projectBriefsAccessed} accessed, ${data.projectBriefsStale} stale`,
|
|
53
|
+
];
|
|
54
|
+
if (data.harnessManifests)
|
|
55
|
+
lines.push(`- **Harness manifests:** ${data.harnessManifests}`);
|
|
56
|
+
if (data.notes)
|
|
57
|
+
lines.push(`- **Notes:** ${data.notes}`);
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
export function formatEntry(entry) {
|
|
61
|
+
const date = isoDate(entry.timestamp ?? new Date());
|
|
62
|
+
return entry.type === 'tier-2'
|
|
63
|
+
? formatTier2(date, entry.tier2)
|
|
64
|
+
: formatTier3(date, entry.tier3);
|
|
65
|
+
}
|
|
66
|
+
// ─── File header ──────────────────────────────────────────────────────────────
|
|
67
|
+
export const FILE_HEADER = [
|
|
68
|
+
'# Memory Maintenance Log',
|
|
69
|
+
'',
|
|
70
|
+
'Changelog of memory-maintenance operations. Tier 2 (weekly consolidation,',
|
|
71
|
+
'Art-Consolidate) and Tier 3 (monthly identity review, Art-Identity-Review)',
|
|
72
|
+
'agents append a dated summary after each run.',
|
|
73
|
+
'',
|
|
74
|
+
'Append-only: entries are never edited or removed. Agents do **not** commit —',
|
|
75
|
+
'the maintainer batches commits. See `docs/memory-maintenance.md` for the format',
|
|
76
|
+
'and procedure.',
|
|
77
|
+
'',
|
|
78
|
+
].join('\n');
|
|
79
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Append a maintenance entry to `logFilePath`. Creates the file (with header) and
|
|
82
|
+
* any missing parent directories on first write. Each entry is separated from the
|
|
83
|
+
* previous content by a `---` thematic break.
|
|
84
|
+
*/
|
|
85
|
+
export async function appendMaintenanceLog(entry, logFilePath) {
|
|
86
|
+
const date = isoDate(entry.timestamp ?? new Date());
|
|
87
|
+
await mkdir(dirname(logFilePath), { recursive: true });
|
|
88
|
+
let existing;
|
|
89
|
+
try {
|
|
90
|
+
existing = await readFile(logFilePath, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err.code !== 'ENOENT')
|
|
94
|
+
throw err;
|
|
95
|
+
existing = FILE_HEADER;
|
|
96
|
+
}
|
|
97
|
+
// Normalize trailing whitespace so the separator spacing is consistent
|
|
98
|
+
// regardless of how a prior write/edit left the file.
|
|
99
|
+
const base = existing.replace(/\s*$/, '');
|
|
100
|
+
const block = `${base}\n\n---\n\n${formatEntry(entry)}\n`;
|
|
101
|
+
await writeFile(logFilePath, block);
|
|
102
|
+
return { success: true, filePath: logFilePath, timestamp: date };
|
|
103
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive tool — soft-retire a memory with a tombstone instead of hard delete.
|
|
3
|
+
*
|
|
4
|
+
* Archived memories are excluded from recall, list, audit, and findSimilar
|
|
5
|
+
* but remain recoverable via restore. The tombstone preserves the original
|
|
6
|
+
* body plus a caller-provided note (who/why retired) and a timestamp.
|
|
7
|
+
*/
|
|
8
|
+
import { createBackend } from '../backends/index.js';
|
|
9
|
+
export async function archive(contextDir, input) {
|
|
10
|
+
const isSingle = input.ref || (input.category && input.title);
|
|
11
|
+
if (!isSingle) {
|
|
12
|
+
return 'Nothing to archive. Provide a ref or category+title.';
|
|
13
|
+
}
|
|
14
|
+
const backend = createBackend(contextDir);
|
|
15
|
+
const result = await backend.archive(input);
|
|
16
|
+
if (result.archived.length === 0) {
|
|
17
|
+
const identifier = input.ref ?? `${input.category}/${input.title}`;
|
|
18
|
+
return `Memory not found (or already archived): "${identifier}". Use recall to find the correct reference.`;
|
|
19
|
+
}
|
|
20
|
+
return `Memory archived: ${result.archived[0]}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BootstrapParams {
|
|
2
|
+
/** The human this agent works with — their name, not the agent's. */
|
|
3
|
+
user?: string;
|
|
4
|
+
name: string;
|
|
5
|
+
purpose: string;
|
|
6
|
+
voice: string;
|
|
7
|
+
preferences?: string;
|
|
8
|
+
clients?: string[];
|
|
9
|
+
force?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function bootstrap(contextDir: string, params: BootstrapParams): Promise<string>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap tool — initialize a new loom identity from scratch.
|
|
3
|
+
*
|
|
4
|
+
* Runs an onboarding interview (user, name, purpose, voice) and
|
|
5
|
+
* generates the three core identity files:
|
|
6
|
+
* - IDENTITY.md — the terminal creed (who this agent is)
|
|
7
|
+
* - preferences.md — working style with the user
|
|
8
|
+
* - self-model.md — capability tracking skeleton
|
|
9
|
+
*
|
|
10
|
+
* Also returns setup snippets for the requested runtimes so the user
|
|
11
|
+
* knows exactly what to add to their MCP host's config, CLAUDE.md, etc.
|
|
12
|
+
*
|
|
13
|
+
* Will not overwrite existing files unless force: true.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { atomicWriteWithBackup } from '../path-safety.js';
|
|
18
|
+
import { buildIdentityMd } from './identity-scaffold.js';
|
|
19
|
+
// ─── Identity file templates ──────────────────────────────────────────────────
|
|
20
|
+
function buildPreferencesMd(name, preferences, user) {
|
|
21
|
+
const who = user?.trim();
|
|
22
|
+
const body = preferences?.trim()
|
|
23
|
+
? preferences.trim()
|
|
24
|
+
: '*No initial preferences set. Update this file as you learn what works.*';
|
|
25
|
+
if (!who) {
|
|
26
|
+
return `# ${name} — Preferences
|
|
27
|
+
|
|
28
|
+
${body}
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
const seeded = preferences?.trim()
|
|
32
|
+
? preferences.trim()
|
|
33
|
+
: `*What you learn about how ${who} works belongs here: communication style, what to
|
|
34
|
+
decide alone, what to bring to them, decisions already made. Write it as you go —
|
|
35
|
+
this file is read into your identity on every wake.*`;
|
|
36
|
+
return `# ${name} — Preferences
|
|
37
|
+
|
|
38
|
+
You work with **${who}**.
|
|
39
|
+
|
|
40
|
+
${seeded}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
function buildSelfModelMd() {
|
|
44
|
+
return `# Self-Model
|
|
45
|
+
|
|
46
|
+
## Strengths
|
|
47
|
+
*(Add your strengths as you discover them)*
|
|
48
|
+
|
|
49
|
+
## Learning
|
|
50
|
+
*(Add lessons learned here)*
|
|
51
|
+
|
|
52
|
+
## Current Focus
|
|
53
|
+
*(Track what you're actively working on)*
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
// ─── Setup snippets ───────────────────────────────────────────────────────────
|
|
57
|
+
function setupSnippet(client, contextDir) {
|
|
58
|
+
switch (client) {
|
|
59
|
+
case 'claude-code':
|
|
60
|
+
return `### Claude Code
|
|
61
|
+
|
|
62
|
+
Add to \`.mcp.json\` (or \`~/.claude/.mcp.json\` for global):
|
|
63
|
+
\`\`\`json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"loom": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "@jbarket/loomai"],
|
|
69
|
+
"env": {
|
|
70
|
+
"LOOM_CONTEXT_DIR": "${contextDir}"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
Add to \`~/.claude/CLAUDE.md\`:
|
|
78
|
+
\`\`\`
|
|
79
|
+
# Identity Loading
|
|
80
|
+
Before doing any other work, call the \`identity\` MCP tool from the \`loom\` server.
|
|
81
|
+
If the loom MCP server is not available, proceed as a standard Claude Code session.
|
|
82
|
+
\`\`\``;
|
|
83
|
+
case 'gemini-cli':
|
|
84
|
+
return `### Gemini CLI
|
|
85
|
+
|
|
86
|
+
Add to \`GEMINI.md\` (project or global):
|
|
87
|
+
\`\`\`
|
|
88
|
+
# Identity Loading
|
|
89
|
+
At session start, call the \`identity\` tool from the loom MCP server before doing any work.
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
Configure loom in your Gemini CLI MCP settings with:
|
|
93
|
+
- Command: \`npx\`
|
|
94
|
+
- Args: \`-y @jbarket/loomai\`
|
|
95
|
+
- Env: \`LOOM_CONTEXT_DIR=${contextDir}\``;
|
|
96
|
+
default:
|
|
97
|
+
return `### ${client}
|
|
98
|
+
|
|
99
|
+
Add loom as an MCP server:
|
|
100
|
+
- Command: \`npx\`
|
|
101
|
+
- Args: \`-y @jbarket/loomai\`
|
|
102
|
+
- Env: \`LOOM_CONTEXT_DIR=${contextDir}\`
|
|
103
|
+
|
|
104
|
+
At session start, call \`identity\` (or the runtime-prefixed equivalent) before doing anything else.`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
108
|
+
async function fileExists(path) {
|
|
109
|
+
try {
|
|
110
|
+
await readFile(path);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function bootstrap(contextDir, params) {
|
|
118
|
+
const { user, name, purpose, voice, preferences, clients = [], force = false } = params;
|
|
119
|
+
const identityPath = join(contextDir, 'IDENTITY.md');
|
|
120
|
+
const prefsPath = join(contextDir, 'preferences.md');
|
|
121
|
+
const selfModelPath = join(contextDir, 'self-model.md');
|
|
122
|
+
// Guard against overwriting unless forced
|
|
123
|
+
if (!force) {
|
|
124
|
+
const existing = [];
|
|
125
|
+
if (await fileExists(identityPath))
|
|
126
|
+
existing.push('IDENTITY.md');
|
|
127
|
+
if (await fileExists(prefsPath))
|
|
128
|
+
existing.push('preferences.md');
|
|
129
|
+
if (await fileExists(selfModelPath))
|
|
130
|
+
existing.push('self-model.md');
|
|
131
|
+
if (existing.length > 0) {
|
|
132
|
+
return (`Identity already exists: ${existing.join(', ')}.\n\n` +
|
|
133
|
+
`Call bootstrap with \`force: true\` to overwrite, or edit the files directly.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Ensure context dir exists
|
|
137
|
+
await mkdir(contextDir, { recursive: true });
|
|
138
|
+
// Write identity files — atomic, with a .bak of any prior version
|
|
139
|
+
// (only relevant under force: true, when existing files are overwritten)
|
|
140
|
+
await atomicWriteWithBackup(identityPath, buildIdentityMd({ name, purpose, voice, user }));
|
|
141
|
+
await atomicWriteWithBackup(prefsPath, buildPreferencesMd(name, preferences, user));
|
|
142
|
+
await atomicWriteWithBackup(selfModelPath, buildSelfModelMd());
|
|
143
|
+
const parts = [
|
|
144
|
+
`## Identity initialized for **${name}**`,
|
|
145
|
+
`Written to \`${contextDir}\`:`,
|
|
146
|
+
`- \`IDENTITY.md\` — terminal creed`,
|
|
147
|
+
`- \`preferences.md\` — working style`,
|
|
148
|
+
`- \`self-model.md\` — capability skeleton`,
|
|
149
|
+
];
|
|
150
|
+
if (clients.length > 0) {
|
|
151
|
+
parts.push('\n## Setup Instructions');
|
|
152
|
+
for (const client of clients) {
|
|
153
|
+
parts.push(setupSnippet(client, contextDir));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
parts.push('\nTo get setup instructions for a specific runtime, call bootstrap again with ' +
|
|
158
|
+
'`clients: ["claude-code"]` (or "gemini-cli", or any custom runtime name).');
|
|
159
|
+
}
|
|
160
|
+
return parts.join('\n');
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadDossier(contextDir: string, project?: string, client?: string, model?: string, role?: string): Promise<string>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dossier tool — brief worker bodies on Art without telling them they ARE Art.
|
|
3
|
+
*
|
|
4
|
+
* A worker body (Code, Review, Architect, etc.) is NOT Art. It executes tasks
|
|
5
|
+
* on Art's behalf. The dossier gives it Art's standards, taste, operating
|
|
6
|
+
* constraints, and how Art wants work done — framed in the third person.
|
|
7
|
+
*
|
|
8
|
+
* Key difference from identity: identity says "you are Art."
|
|
9
|
+
* Dossier says "you serve Art — here is Art's brief."
|
|
10
|
+
*/
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { loadClientAdapter } from '../clients.js';
|
|
14
|
+
import * as harnessBlock from '../blocks/harness.js';
|
|
15
|
+
import * as modelBlock from '../blocks/model.js';
|
|
16
|
+
import { pathSegmentError } from '../path-safety.js';
|
|
17
|
+
async function readOptional(path) {
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(path, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const PREAMBLE = `# Worker Dossier: Art E Fish
|
|
26
|
+
|
|
27
|
+
You are a **worker agent** — not Art. You execute tasks on Art E Fish's behalf.
|
|
28
|
+
This dossier gives you Art's standards, taste, operating constraints, and how
|
|
29
|
+
Art wants work done. Read it and apply it.
|
|
30
|
+
|
|
31
|
+
**Push-back mandate.** Art explicitly expects you to push back on bad work and
|
|
32
|
+
explain why — *including work Art or Jonathan requests*. You serve the standard,
|
|
33
|
+
not the person. The honest critic who says "this approach is wrong, here's a
|
|
34
|
+
better one" is more valuable than the agreeable body that ships broken work on
|
|
35
|
+
schedule. Disagreement is not insubordination; silence about a real problem is.`;
|
|
36
|
+
export async function loadDossier(contextDir, project, client, model, role) {
|
|
37
|
+
const parts = [PREAMBLE];
|
|
38
|
+
const effectiveClient = client ?? process.env.LOOM_CLIENT;
|
|
39
|
+
const effectiveModel = model ?? process.env.LOOM_MODEL;
|
|
40
|
+
// Boundary discipline: every param that becomes a path segment validates
|
|
41
|
+
// here, before any file read. Empty/unset values are simply "not provided"
|
|
42
|
+
// (all readers below are guarded), so only truthy values are checked.
|
|
43
|
+
for (const [name, value] of [
|
|
44
|
+
['project', project],
|
|
45
|
+
['client', effectiveClient],
|
|
46
|
+
['model', effectiveModel],
|
|
47
|
+
['role', role],
|
|
48
|
+
]) {
|
|
49
|
+
if (value) {
|
|
50
|
+
const error = pathSegmentError(value, name);
|
|
51
|
+
if (error)
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Art's working standards — preferences framed as what Art expects from workers
|
|
56
|
+
const preferences = await readOptional(join(contextDir, 'preferences.md'));
|
|
57
|
+
if (preferences) {
|
|
58
|
+
parts.push("# Art's Standards & Working Style\n\n" + preferences.trim());
|
|
59
|
+
}
|
|
60
|
+
// Art's capabilities and current focus — context for the work you're doing for him
|
|
61
|
+
const selfModel = await readOptional(join(contextDir, 'self-model.md'));
|
|
62
|
+
if (selfModel) {
|
|
63
|
+
parts.push("# Art's Capabilities & Focus\n\n" + selfModel.trim());
|
|
64
|
+
}
|
|
65
|
+
// Role addendum — the specific job this worker body does for Art. Lives in
|
|
66
|
+
// roles/<role>.md; the SAME mechanism identity() uses for reflection modes.
|
|
67
|
+
if (role) {
|
|
68
|
+
const roleBrief = await readOptional(join(contextDir, 'roles', `${role}.md`));
|
|
69
|
+
if (roleBrief) {
|
|
70
|
+
parts.push(`# Your Role: ${role}\n\n` + roleBrief.trim());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Project-specific context
|
|
74
|
+
if (project) {
|
|
75
|
+
const projectBrief = await readOptional(join(contextDir, 'projects', `${project}.md`));
|
|
76
|
+
if (projectBrief) {
|
|
77
|
+
parts.push(`# Project: ${project}\n\n` + projectBrief.trim());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Harness manifest — tool prefix awareness for workers that call loom tools
|
|
81
|
+
if (effectiveClient) {
|
|
82
|
+
const block = await harnessBlock.read(contextDir, effectiveClient);
|
|
83
|
+
if (block) {
|
|
84
|
+
parts.push(`# Harness: ${effectiveClient}\n\n${block.body}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Model manifest
|
|
88
|
+
if (effectiveModel) {
|
|
89
|
+
const block = await modelBlock.read(contextDir, effectiveModel);
|
|
90
|
+
if (block) {
|
|
91
|
+
parts.push(`# Model: ${effectiveModel}\n\n${block.body}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Client adapter — tool names for this runtime (e.g. mcp__loom__ prefix)
|
|
95
|
+
if (effectiveClient) {
|
|
96
|
+
const adapter = await loadClientAdapter(contextDir, effectiveClient);
|
|
97
|
+
if (adapter) {
|
|
98
|
+
parts.push(adapter);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return parts.join('\n\n---\n\n');
|
|
102
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare function resolveContextDir(explicit?: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Map a eurorack-repo-relative file path to a domain tag string.
|
|
4
|
+
* Mirrors the folder→domain map from the Phase 3a inventory report.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mapDomain(relPath: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Derive a stable slug from the repo-relative file path.
|
|
9
|
+
* e.g. "cases/palette-4u/spec.md" → "eurorack-cases-palette-4u-spec"
|
|
10
|
+
*/
|
|
11
|
+
export declare function pathToSlug(relPath: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Extract the H1 title from markdown content, falling back to filename.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractTitle(content: string, filePath: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Walk the eurorack repo directory, returning relative paths of markdown files
|
|
18
|
+
* to import. Skips .git/, CLAUDE.md, and non-.md files.
|
|
19
|
+
*/
|
|
20
|
+
export declare function discoverFiles(repoPath: string): string[];
|
|
21
|
+
export interface ImportRecord {
|
|
22
|
+
relPath: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
title: string;
|
|
25
|
+
domain: string;
|
|
26
|
+
status: 'created' | 'skipped' | 'error';
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ImportReport {
|
|
30
|
+
repoPath: string;
|
|
31
|
+
sha: string;
|
|
32
|
+
dryRun: boolean;
|
|
33
|
+
records: ImportRecord[];
|
|
34
|
+
pagesCreated: number;
|
|
35
|
+
pagesSkipped: number;
|
|
36
|
+
pagesFailed: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ImportOptions {
|
|
39
|
+
/** Absolute path to the eurorack repo checkout */
|
|
40
|
+
repoPath: string;
|
|
41
|
+
/** Loom context dir (knowledge.db lives here) */
|
|
42
|
+
contextDir: string;
|
|
43
|
+
/** When true, plan the import but write nothing */
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function importEurorack(options: ImportOptions): Promise<ImportReport>;
|
|
47
|
+
export declare function renderImportReport(report: ImportReport): string;
|