mishkan-harness 0.1.0 → 0.2.3
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 +121 -134
- package/bin/mishkan.js +810 -26
- package/docs/design/MISHKAN_decisions.md +842 -0
- package/docs/design/MISHKAN_harness_design.md +1 -33
- package/docs/design/MISHKAN_observability.md +1027 -0
- package/docs/research/graphify-token-saving-poc.md +108 -0
- package/docs/usage/01-installation.md +40 -12
- package/docs/usage/09-workflows.md +74 -4
- package/docs/usage/10-observability.md +173 -0
- package/docs/usage/11-graphify.md +264 -0
- package/docs/usage/12-skill-discovery.md +294 -0
- package/docs/usage/README.md +5 -2
- package/package.json +1 -1
- package/payload/install/settings.hooks.json +33 -0
- package/payload/mishkan/AGENT_SPEC.md +25 -7
- package/payload/mishkan/agents/ahikam.md +4 -2
- package/payload/mishkan/agents/aholiab.md +4 -2
- package/payload/mishkan/agents/asaph.md +5 -2
- package/payload/mishkan/agents/baruch.md +19 -2
- package/payload/mishkan/agents/benaiah.md +4 -2
- package/payload/mishkan/agents/bezalel.md +4 -2
- package/payload/mishkan/agents/caleb.md +4 -2
- package/payload/mishkan/agents/deborah.md +4 -2
- package/payload/mishkan/agents/elasah.md +4 -2
- package/payload/mishkan/agents/eliashib.md +4 -2
- package/payload/mishkan/agents/ezra.md +4 -2
- package/payload/mishkan/agents/hanun.md +5 -2
- package/payload/mishkan/agents/hiram.md +5 -2
- package/payload/mishkan/agents/hizkiah.md +5 -2
- package/payload/mishkan/agents/huldah.md +4 -2
- package/payload/mishkan/agents/huram.md +4 -2
- package/payload/mishkan/agents/hushai.md +5 -2
- package/payload/mishkan/agents/igal.md +4 -2
- package/payload/mishkan/agents/ira.md +5 -2
- package/payload/mishkan/agents/jahaziel.md +5 -2
- package/payload/mishkan/agents/jakin.md +4 -2
- package/payload/mishkan/agents/jehonathan.md +5 -2
- package/payload/mishkan/agents/jehoshaphat.md +4 -2
- package/payload/mishkan/agents/joab.md +5 -2
- package/payload/mishkan/agents/joah.md +5 -2
- package/payload/mishkan/agents/maaseiah.md +4 -2
- package/payload/mishkan/agents/meremoth.md +5 -2
- package/payload/mishkan/agents/meshullam.md +5 -2
- package/payload/mishkan/agents/nathan.md +5 -2
- package/payload/mishkan/agents/nehemiah.md +4 -2
- package/payload/mishkan/agents/obed.md +4 -2
- package/payload/mishkan/agents/oholiab.md +5 -2
- package/payload/mishkan/agents/palal.md +5 -2
- package/payload/mishkan/agents/phinehas.md +4 -2
- package/payload/mishkan/agents/rehum.md +4 -2
- package/payload/mishkan/agents/salma.md +5 -2
- package/payload/mishkan/agents/seraiah.md +4 -2
- package/payload/mishkan/agents/shallum.md +5 -2
- package/payload/mishkan/agents/shaphan.md +4 -2
- package/payload/mishkan/agents/shemaiah.md +4 -2
- package/payload/mishkan/agents/shevna.md +5 -2
- package/payload/mishkan/agents/uriah.md +5 -2
- package/payload/mishkan/agents/zaccur.md +4 -2
- package/payload/mishkan/agents/zadok.md +5 -2
- package/payload/mishkan/agents/zerubbabel.md +4 -2
- package/payload/mishkan/commands/code-graph.md +35 -0
- package/payload/mishkan/commands/eval-baruch.md +57 -0
- package/payload/mishkan/commands/mishkan-org-reference.md +58 -0
- package/payload/mishkan/commands/mishkan-skills-misses.md +47 -0
- package/payload/mishkan/commands/mishkan-skills-reindex.md +33 -0
- package/payload/mishkan/commands/skills.md +35 -0
- package/payload/mishkan/commands/sprint-close.md +3 -1
- package/payload/mishkan/hooks/model-route.py +32 -0
- package/payload/mishkan/hooks/post-tool-observe.sh +222 -14
- package/payload/mishkan/hooks/pre-tool-knowledge-route.sh +231 -0
- package/payload/mishkan/hooks/pre-tool-security.sh +16 -0
- package/payload/mishkan/hooks/pre-tool-task-skill-route.sh +122 -0
- package/payload/mishkan/hooks/pre-tool-trace.sh +43 -0
- package/payload/mishkan/hooks/session-start-skill-index.sh +35 -0
- package/payload/mishkan/observability/README.md +100 -0
- package/payload/mishkan/observability/bus.py +89 -0
- package/payload/mishkan/observability/bus.sh +117 -0
- package/payload/mishkan/observability/schema.json +209 -0
- package/payload/mishkan/observability/usage_parser.py +211 -0
- package/payload/mishkan/observability/watch/README.md +62 -0
- package/payload/mishkan/observability/watch/pyproject.toml +22 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/__init__.py +3 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/__main__.py +111 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/app.py +296 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/client.py +134 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/org_data.py +79 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/skills_data.py +267 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/__init__.py +1 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/activity.py +100 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/agents.py +310 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/knowledge.py +297 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/live.py +407 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/org.py +139 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/skills.py +187 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/usage.py +377 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/workflows.py +361 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/theme.tcss +328 -0
- package/payload/mishkan/observability/watchd/README.md +59 -0
- package/payload/mishkan/observability/watchd/pyproject.toml +23 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/__init__.py +3 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/__main__.py +260 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/lifecycle.py +68 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/server.py +134 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/__init__.py +6 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/bus_tail.py +110 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/cognee_poll.py +228 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/graphify_tail.py +220 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/mcp_probe.py +230 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/session_discover.py +74 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/session_tail.py +161 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/subagent_tail.py +195 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/worktree_poll.py +118 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/state.py +442 -0
- package/payload/mishkan/observability/watchd/tests/test_state.py +81 -0
- package/payload/mishkan/org/org.json +141 -0
- package/payload/mishkan/scripts/pre-commit-eval-gate.sh +50 -0
- package/payload/mishkan/scripts/skill-discovery-indexer.py +462 -0
- package/payload/mishkan/scripts/skill-discovery-misses.py +173 -0
- package/payload/mishkan/scripts/skill-discovery-router.py +457 -0
- package/payload/mishkan/scripts/skill-discovery-smoke.sh +156 -0
- package/payload/mishkan/skills/graphify-query-craft/SKILL.md +162 -0
- package/payload/mishkan/skills/mishkan-init/SKILL.md +15 -0
- package/payload/mishkan/skills/skill-discovery/SKILL.md +120 -0
- package/payload/mishkan/templates/case-node.schema.json +1 -0
- package/payload/mishkan/templates/observability-log.schema.json +1 -0
- package/payload/mishkan/workflows/README.md +78 -13
- package/payload/mishkan/workflows/chosheb-feature-ship.js +75 -0
- package/payload/mishkan/workflows/migdal-dr-drill.js +94 -0
- package/payload/mishkan/workflows/migdal-infra-change.js +67 -0
- package/payload/mishkan/workflows/mishkan-blast-radius.js +144 -0
- package/payload/mishkan/workflows/mishkan-init.js +58 -0
- package/payload/mishkan/workflows/mishkan-knowledge-gap-discovery.js +121 -0
- package/payload/mishkan/workflows/mishkan-standards-rollout.js +153 -0
- package/payload/mishkan/workflows/mishmar-security-gate.js +83 -0
- package/payload/mishkan/workflows/panim-ds-rollout.js +83 -0
- package/payload/mishkan/workflows/sefer-release-notes.js +85 -0
- package/payload/mishkan/workflows/yasad-data-migration-wave.js +78 -0
- package/payload/mishkan/workflows/yasad-schema-evolution.js +76 -0
- package/payload/mishkan/templates/user-CLAUDE.md +0 -62
package/bin/mishkan.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// MISHKAN installer — dependency-free (Node >=18, built-ins only).
|
|
3
|
-
// Commands: install | uninstall | status
|
|
3
|
+
// Commands: install | uninstall | status | observability
|
|
4
4
|
//
|
|
5
5
|
// Portability by design: every path is resolved from os.homedir() at runtime.
|
|
6
6
|
// No machine-specific paths are baked in. Idempotent: re-running install updates
|
|
7
7
|
// in place. Never clobbers user-edited files (CLAUDE.md, rules, real agents).
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync,
|
|
9
|
-
copyFileSync, lstatSync, readlinkSync, symlinkSync, rmSync, statSync
|
|
9
|
+
copyFileSync, lstatSync, readlinkSync, symlinkSync, rmSync, statSync,
|
|
10
|
+
chmodSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { join, dirname, relative } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { createInterface } from "node:readline";
|
|
16
|
+
import { stdin, stdout } from "node:process";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
13
18
|
|
|
14
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
20
|
const PKG = join(HERE, "..");
|
|
@@ -18,8 +23,30 @@ const CLAUDE = join(HOME, ".claude");
|
|
|
18
23
|
const MISHKAN = join(CLAUDE, "mishkan");
|
|
19
24
|
const STAMP = join(MISHKAN, ".install-stamp");
|
|
20
25
|
|
|
26
|
+
// ─── output ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
29
|
+
const c = NO_COLOR
|
|
30
|
+
? { dim: s => s, bold: s => s, cyan: s => s, green: s => s, yellow: s => s, red: s => s }
|
|
31
|
+
: {
|
|
32
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
33
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
34
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
35
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
36
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
37
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
38
|
+
};
|
|
39
|
+
|
|
21
40
|
const log = (...a) => console.log("mishkan:", ...a);
|
|
22
|
-
const warn = (...a) => console.warn("mishkan: WARN", ...a);
|
|
41
|
+
const warn = (...a) => console.warn("mishkan: " + c.yellow("WARN"), ...a);
|
|
42
|
+
|
|
43
|
+
// Print a phase header with a one-line "why" subtitle. Helps the engineer
|
|
44
|
+
// see what each step does and why, instead of an unstructured wall of logs.
|
|
45
|
+
function phase(n, total, title, why) {
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(c.bold(c.cyan(`[${n}/${total}] ${title}`)));
|
|
48
|
+
if (why) console.log(c.dim(` ${why}`));
|
|
49
|
+
}
|
|
23
50
|
|
|
24
51
|
function ensureDir(d) { mkdirSync(d, { recursive: true }); }
|
|
25
52
|
|
|
@@ -106,16 +133,504 @@ function removeHooks() {
|
|
|
106
133
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
|
|
136
|
+
// ─── tool availability + interactive prompts ──────────────────────────────
|
|
137
|
+
|
|
138
|
+
function commandExists(cmd) {
|
|
139
|
+
const r = spawnSync(process.platform === "win32" ? "where" : "which",
|
|
140
|
+
[cmd], { stdio: "ignore" });
|
|
141
|
+
return r.status === 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function promptYN(question, defaultYes = true) {
|
|
145
|
+
// Non-TTY (CI, piped install) -> use the default, never block.
|
|
146
|
+
if (!stdin.isTTY) return defaultYes;
|
|
147
|
+
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
148
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
149
|
+
const ans = await new Promise(r => rl.question(question + suffix, r));
|
|
150
|
+
rl.close();
|
|
151
|
+
const a = ans.trim().toLowerCase();
|
|
152
|
+
if (!a) return defaultYes;
|
|
153
|
+
return a === "y" || a === "yes" || a === "o" || a === "oui";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function promptText(question) {
|
|
157
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
158
|
+
const ans = await new Promise(r => rl.question(question, r));
|
|
159
|
+
rl.close();
|
|
160
|
+
return ans.trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Masked input: each typed char becomes "*". Ctrl-C aborts. Backspace works.
|
|
164
|
+
// Non-TTY: falls back to a normal readline with a one-line warning.
|
|
165
|
+
async function promptSecret(question) {
|
|
166
|
+
if (!stdin.isTTY) {
|
|
167
|
+
warn("non-TTY stdin — secret input will be visible.");
|
|
168
|
+
return promptText(question);
|
|
169
|
+
}
|
|
170
|
+
stdout.write(question);
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
let buf = "";
|
|
173
|
+
stdin.setRawMode(true);
|
|
174
|
+
stdin.resume();
|
|
175
|
+
stdin.setEncoding("utf8");
|
|
176
|
+
const onData = (ch) => {
|
|
177
|
+
if (ch === "\r" || ch === "\n") {
|
|
178
|
+
stdin.setRawMode(false);
|
|
179
|
+
stdin.pause();
|
|
180
|
+
stdin.removeListener("data", onData);
|
|
181
|
+
stdout.write("\n");
|
|
182
|
+
resolve(buf);
|
|
183
|
+
} else if (ch === "") { // Ctrl-C
|
|
184
|
+
stdin.setRawMode(false);
|
|
185
|
+
stdin.pause();
|
|
186
|
+
stdin.removeListener("data", onData);
|
|
187
|
+
stdout.write("\n");
|
|
188
|
+
process.exit(130);
|
|
189
|
+
} else if (ch === "" || ch === "") { // backspace
|
|
190
|
+
if (buf.length > 0) { buf = buf.slice(0, -1); stdout.write("\b \b"); }
|
|
191
|
+
} else if (ch >= " " && ch.length === 1) {
|
|
192
|
+
buf += ch;
|
|
193
|
+
stdout.write("*");
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
stdin.on("data", onData);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function promptChoice(question, choices) {
|
|
201
|
+
if (!stdin.isTTY) return choices[0]?.key;
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(c.bold(question));
|
|
204
|
+
for (const ch of choices) {
|
|
205
|
+
console.log(` ${c.cyan(ch.key)}) ${c.bold(ch.name)}`);
|
|
206
|
+
if (ch.why) console.log(c.dim(` ${ch.why}`));
|
|
207
|
+
}
|
|
208
|
+
const valid = new Set(choices.map(c => c.key.toUpperCase()));
|
|
209
|
+
while (true) {
|
|
210
|
+
const ans = (await promptText(` Choose [${choices.map(c => c.key).join("/")}]: `)).toUpperCase();
|
|
211
|
+
if (valid.has(ans)) return ans;
|
|
212
|
+
console.log(c.yellow(` ? not a valid choice`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Knowledge stack configuration wizard ─────────────────────────────────
|
|
217
|
+
// Configures the whole cognee .env: LLM provider + key(s), neo4j/pg/admin
|
|
218
|
+
// secrets (generated or preserved), admin email. Without this, the stack
|
|
219
|
+
// doesn't have enough env to bring docker-compose up.
|
|
220
|
+
|
|
221
|
+
const LLM_PROFILES = {
|
|
222
|
+
A: {
|
|
223
|
+
name: "Ollama (fully self-hosted)",
|
|
224
|
+
why: "No key, no quota. Free but slower for cognify. Recommended for personal hosts.",
|
|
225
|
+
needs: [],
|
|
226
|
+
block: () => `# Provider profile: Ollama (local, no external dependency)
|
|
227
|
+
LLM_PROVIDER=ollama
|
|
228
|
+
LLM_MODEL=qwen2.5:3b
|
|
229
|
+
LLM_ENDPOINT=http://ollama:11434/v1
|
|
230
|
+
LLM_API_KEY=ollama
|
|
231
|
+
EMBEDDING_PROVIDER=ollama
|
|
232
|
+
EMBEDDING_MODEL=nomic-embed-text:latest
|
|
233
|
+
EMBEDDING_ENDPOINT=http://ollama:11434/api/embed
|
|
234
|
+
EMBEDDING_DIMENSIONS=768`,
|
|
235
|
+
},
|
|
236
|
+
B: {
|
|
237
|
+
name: "Google Gemini (cloud, paired LLM+embed)",
|
|
238
|
+
why: "Generous free tier at Google AI Studio. Single key. Strong for cognify volume.",
|
|
239
|
+
needs: [{ key: "LLM_API_KEY", prompt: " Google AI Studio key: " }],
|
|
240
|
+
block: (k) => `# Provider profile: Google Gemini (paired LLM + embeddings)
|
|
241
|
+
LLM_PROVIDER=gemini
|
|
242
|
+
LLM_MODEL=gemini/gemini-2.5-flash
|
|
243
|
+
LLM_API_KEY=${k.LLM_API_KEY}
|
|
244
|
+
EMBEDDING_PROVIDER=gemini
|
|
245
|
+
EMBEDDING_MODEL=gemini/gemini-embedding-001
|
|
246
|
+
EMBEDDING_DIMENSIONS=3072`,
|
|
247
|
+
},
|
|
248
|
+
C: {
|
|
249
|
+
name: "OpenAI (cloud, paired LLM+embed)",
|
|
250
|
+
why: "Pay per token, no free tier. Single key. Predictable quality.",
|
|
251
|
+
needs: [{ key: "LLM_API_KEY", prompt: " OpenAI API key (sk-...): " }],
|
|
252
|
+
block: (k) => `# Provider profile: OpenAI (paired LLM + embeddings)
|
|
253
|
+
LLM_PROVIDER=openai
|
|
254
|
+
LLM_MODEL=openai/gpt-5-mini
|
|
255
|
+
LLM_API_KEY=${k.LLM_API_KEY}
|
|
256
|
+
EMBEDDING_PROVIDER=openai
|
|
257
|
+
EMBEDDING_MODEL=openai/text-embedding-3-large
|
|
258
|
+
EMBEDDING_DIMENSIONS=3072`,
|
|
259
|
+
},
|
|
260
|
+
D: {
|
|
261
|
+
name: "Anthropic LLM + OpenAI embeddings (TWO keys)",
|
|
262
|
+
why: "Claude for reasoning; OpenAI for embed (Claude has none). Two providers, two keys.",
|
|
263
|
+
needs: [
|
|
264
|
+
{ key: "LLM_API_KEY", prompt: " Anthropic API key (sk-ant-...): " },
|
|
265
|
+
{ key: "EMBEDDING_API_KEY", prompt: " OpenAI API key for embeddings (sk-...): " },
|
|
266
|
+
],
|
|
267
|
+
block: (k) => `# Provider profile: Anthropic LLM + OpenAI embeddings
|
|
268
|
+
LLM_PROVIDER=anthropic
|
|
269
|
+
LLM_MODEL=anthropic/claude-sonnet-4-5
|
|
270
|
+
LLM_API_KEY=${k.LLM_API_KEY}
|
|
271
|
+
EMBEDDING_PROVIDER=openai
|
|
272
|
+
EMBEDDING_MODEL=openai/text-embedding-3-large
|
|
273
|
+
EMBEDDING_DIMENSIONS=3072
|
|
274
|
+
EMBEDDING_API_KEY=${k.EMBEDDING_API_KEY}`,
|
|
275
|
+
},
|
|
276
|
+
E: {
|
|
277
|
+
name: "NVIDIA API Catalog + Ollama embeddings",
|
|
278
|
+
why: "Free NVIDIA cloud LLM (rate-limited, OpenAI-compatible) + local Ollama embed.",
|
|
279
|
+
needs: [{ key: "LLM_API_KEY", prompt: " NVIDIA nvapi-... key (build.nvidia.com): " }],
|
|
280
|
+
block: (k) => `# Provider profile: NVIDIA API Catalog (LLM) + Ollama (embeddings)
|
|
281
|
+
LLM_PROVIDER=custom
|
|
282
|
+
LLM_MODEL=openai/meta/llama-3.1-70b-instruct
|
|
283
|
+
LLM_ENDPOINT=https://integrate.api.nvidia.com/v1
|
|
284
|
+
LLM_API_KEY=${k.LLM_API_KEY}
|
|
285
|
+
EMBEDDING_PROVIDER=ollama
|
|
286
|
+
EMBEDDING_MODEL=nomic-embed-text:latest
|
|
287
|
+
EMBEDDING_ENDPOINT=http://ollama:11434/api/embed
|
|
288
|
+
EMBEDDING_DIMENSIONS=768`,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Parse a KEY=VALUE env file into a flat dict (ignores comments / blank lines).
|
|
293
|
+
function parseEnv(text) {
|
|
294
|
+
const out = {};
|
|
295
|
+
for (const raw of text.split("\n")) {
|
|
296
|
+
const line = raw.trim();
|
|
297
|
+
if (!line || line.startsWith("#")) continue;
|
|
298
|
+
const eq = line.indexOf("=");
|
|
299
|
+
if (eq === -1) continue;
|
|
300
|
+
const k = line.slice(0, eq).trim();
|
|
301
|
+
const v = line.slice(eq + 1);
|
|
302
|
+
out[k] = v;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 32-char URL-safe random secret. Strong enough for local-only daemons.
|
|
308
|
+
function genSecret() { return randomBytes(16).toString("hex"); }
|
|
309
|
+
|
|
310
|
+
async function configureKnowledge() {
|
|
311
|
+
const targetDir = join(MISHKAN, "cognee");
|
|
312
|
+
const exampleEnv = join(targetDir, ".env.example");
|
|
313
|
+
const targetEnv = join(targetDir, ".env");
|
|
314
|
+
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(c.bold(c.cyan("Configure the knowledge stack")));
|
|
317
|
+
console.log(c.dim(
|
|
318
|
+
" Writes ~/.claude/mishkan/cognee/.env (0600, gitignored).\n" +
|
|
319
|
+
" Sets the LLM provider + keys, generates neo4j + postgres + admin\n" +
|
|
320
|
+
" passwords on a fresh install, preserves them on a re-run so an\n" +
|
|
321
|
+
" initialised neo4j volume keeps working."));
|
|
322
|
+
|
|
323
|
+
if (!existsSync(targetDir)) {
|
|
324
|
+
warn("cognee dir missing. Run `mishkan-harness install` first.");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!existsSync(exampleEnv)) {
|
|
328
|
+
warn(`template missing: ${tilde(exampleEnv)}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Preserve existing local secrets if a .env is already initialised — neo4j
|
|
333
|
+
// and pg volumes are encrypted-at-rest with their first-boot password and
|
|
334
|
+
// would lock us out if we regenerated.
|
|
335
|
+
let preserved = {};
|
|
336
|
+
if (existsSync(targetEnv)) {
|
|
337
|
+
const ok = await promptYN(` .env exists. Overwrite (preserving the 3 local secrets)?`, false);
|
|
338
|
+
if (!ok) { console.log(c.dim(" Aborted.")); return; }
|
|
339
|
+
copyFileSync(targetEnv, targetEnv + ".bak");
|
|
340
|
+
preserved = parseEnv(readFileSync(targetEnv, "utf8"));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 1) provider profile
|
|
344
|
+
const profileKey = await promptChoice("LLM provider profiles:",
|
|
345
|
+
Object.entries(LLM_PROFILES).map(([key, p]) => ({ key, name: p.name, why: p.why })));
|
|
346
|
+
const profile = LLM_PROFILES[profileKey];
|
|
347
|
+
|
|
348
|
+
// 2) provider keys (masked)
|
|
349
|
+
const apiKeys = {};
|
|
350
|
+
for (const need of profile.needs) {
|
|
351
|
+
const v = await promptSecret(need.prompt);
|
|
352
|
+
if (!v) { console.log(c.yellow(" Aborted (empty key).")); return; }
|
|
353
|
+
apiKeys[need.key] = v;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 3) admin email (cognee backend bootstrap user)
|
|
357
|
+
const defaultEmail = preserved.DEFAULT_USER_EMAIL || "admin@local.dev";
|
|
358
|
+
const emailIn = await promptText(` Cognee admin email [${defaultEmail}]: `);
|
|
359
|
+
const adminEmail = emailIn || defaultEmail;
|
|
360
|
+
|
|
361
|
+
// 4) stack secrets — preserve if .env existed, else generate
|
|
362
|
+
const stackSecrets = {
|
|
363
|
+
GRAPH_DATABASE_USERNAME: preserved.GRAPH_DATABASE_USERNAME || "neo4j",
|
|
364
|
+
GRAPH_DATABASE_PASSWORD: preserved.GRAPH_DATABASE_PASSWORD || genSecret(),
|
|
365
|
+
DB_PASSWORD: preserved.DB_PASSWORD || genSecret(),
|
|
366
|
+
DEFAULT_USER_PASSWORD: preserved.DEFAULT_USER_PASSWORD || genSecret(),
|
|
367
|
+
};
|
|
368
|
+
const generated = Object.entries(stackSecrets).filter(([k]) => !preserved[k]).map(([k]) => k);
|
|
369
|
+
|
|
370
|
+
// 5) build the .env. Strip provider + stack-secret lines from the example
|
|
371
|
+
// so our explicit blocks land cleanly, then append the rest verbatim so the
|
|
372
|
+
// engineer's tuning (rate limits, common defaults) survives.
|
|
373
|
+
const example = readFileSync(exampleEnv, "utf8");
|
|
374
|
+
const strip = /^(LLM|EMBEDDING)_(API_KEY|PROVIDER|MODEL|ENDPOINT|DIMENSIONS)=|^(GRAPH_DATABASE_USERNAME|GRAPH_DATABASE_PASSWORD|DB_PASSWORD|DEFAULT_USER_EMAIL|DEFAULT_USER_PASSWORD)=/;
|
|
375
|
+
const tail = example.split("\n").filter(l => !strip.test(l)).join("\n");
|
|
376
|
+
|
|
377
|
+
const stackBlock = `# Local-only stack secrets (neo4j, postgres, cognee backend admin).
|
|
378
|
+
# Generated fresh on first run; preserved across re-runs of configure-knowledge.
|
|
379
|
+
GRAPH_DATABASE_USERNAME=${stackSecrets.GRAPH_DATABASE_USERNAME}
|
|
380
|
+
GRAPH_DATABASE_PASSWORD=${stackSecrets.GRAPH_DATABASE_PASSWORD}
|
|
381
|
+
DB_PASSWORD=${stackSecrets.DB_PASSWORD}
|
|
382
|
+
DEFAULT_USER_EMAIL=${adminEmail}
|
|
383
|
+
DEFAULT_USER_PASSWORD=${stackSecrets.DEFAULT_USER_PASSWORD}`;
|
|
384
|
+
|
|
385
|
+
const final = `# Generated by mishkan-harness configure-knowledge on ${new Date().toISOString()}
|
|
386
|
+
# Profile: ${profile.name}
|
|
387
|
+
# Re-run \`npx mishkan-harness configure-knowledge\` to switch providers (secrets preserved).
|
|
388
|
+
|
|
389
|
+
${profile.block(apiKeys)}
|
|
390
|
+
|
|
391
|
+
${stackBlock}
|
|
392
|
+
|
|
393
|
+
${tail}`;
|
|
394
|
+
|
|
395
|
+
writeFileSync(targetEnv, final, { mode: 0o600 });
|
|
396
|
+
|
|
397
|
+
// Companion file: ACCESS.txt with every URL, every credential, and the
|
|
398
|
+
// remote-SSH-tunnel howto. Lives next to .env, same 0600 perms, gitignored
|
|
399
|
+
// because it carries plaintext passwords.
|
|
400
|
+
const accessPath = join(targetDir, "ACCESS.txt");
|
|
401
|
+
writeFileSync(accessPath, renderAccessTxt({
|
|
402
|
+
profile,
|
|
403
|
+
adminEmail,
|
|
404
|
+
secrets: stackSecrets,
|
|
405
|
+
}), { mode: 0o600 });
|
|
406
|
+
|
|
407
|
+
console.log();
|
|
408
|
+
console.log(c.green(`✓ wrote ${tilde(targetEnv)} (0600)`));
|
|
409
|
+
console.log(c.green(`✓ wrote ${tilde(accessPath)} (0600) — URLs + creds + connection guide`));
|
|
410
|
+
console.log(c.dim(` Profile: ${profile.name}`));
|
|
411
|
+
if (generated.length) {
|
|
412
|
+
console.log(c.yellow(` Generated fresh: ${generated.join(", ")}`));
|
|
413
|
+
console.log(c.dim(" All creds are in ACCESS.txt; back it up if you need them outside this host."));
|
|
414
|
+
} else {
|
|
415
|
+
console.log(c.dim(" Preserved existing neo4j / pg / admin secrets."));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Live summary in the terminal — quick reference even without opening
|
|
419
|
+
// the file. URLs come straight from the docker-compose port mappings.
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(c.bold("Quick reference (also written to ACCESS.txt):"));
|
|
422
|
+
console.log(c.dim(" ──────────────────────────────────────────────────────────────"));
|
|
423
|
+
console.log(` Work · MCP ${c.cyan("http://127.0.0.1:7777/mcp")}`);
|
|
424
|
+
console.log(` Work · Graph UI ${c.cyan("http://127.0.0.1:7724")} ${c.dim(`${adminEmail} / DEFAULT_USER_PASSWORD`)}`);
|
|
425
|
+
console.log(` Work · Neo4j ${c.cyan("http://127.0.0.1:7716")} ${c.dim("neo4j / GRAPH_DATABASE_PASSWORD")}`);
|
|
426
|
+
console.log(` Work · REST ${c.cyan("http://127.0.0.1:7737")}`);
|
|
427
|
+
console.log(` Curated · MCP ${c.cyan("http://127.0.0.1:7730/mcp")}`);
|
|
428
|
+
console.log(` Curated · Graph ${c.cyan("http://127.0.0.1:7734")}`);
|
|
429
|
+
console.log(` Curated · Neo4j ${c.cyan("http://127.0.0.1:7731")}`);
|
|
430
|
+
console.log(` Curated · REST ${c.cyan("http://127.0.0.1:7733")}`);
|
|
431
|
+
if (profile.name.includes("Ollama")) {
|
|
432
|
+
console.log(` Ollama ${c.cyan("http://127.0.0.1:11434")} ${c.dim("local LLM / embeddings")}`);
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
console.log(c.bold("Bring up the Cognee stack:"));
|
|
436
|
+
console.log(c.dim(` cd ${tilde(targetDir)}`));
|
|
437
|
+
console.log(c.dim(" docker compose -f docker-compose.yml -f docker-compose.hardening.yml up -d --build"));
|
|
438
|
+
console.log(c.dim(`\n Full guide (incl. SSH-tunnel for remote hosts): ${tilde(accessPath)}`));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Human-readable access guide written alongside .env. Sectioned plain text,
|
|
442
|
+
// 0600, gitignored. Contains every URL the Cognee stack exposes + the
|
|
443
|
+
// credentials each endpoint needs + the SSH-tunnel howto for remote access.
|
|
444
|
+
function renderAccessTxt({ profile, adminEmail, secrets }) {
|
|
445
|
+
const sep = "─".repeat(70);
|
|
446
|
+
const stamp = new Date().toISOString();
|
|
447
|
+
const ollamaSection = profile.name.includes("Ollama")
|
|
448
|
+
? `\n${sep}\nOllama (local LLM / embeddings)\n${sep}\n
|
|
449
|
+
URL : http://127.0.0.1:11434
|
|
450
|
+
Models : qwen2.5:3b (LLM), nomic-embed-text:latest (embeddings)
|
|
451
|
+
Auth : none (local)
|
|
452
|
+
Health : curl -fsS http://127.0.0.1:11434/api/tags
|
|
453
|
+
|
|
454
|
+
Used by : the Cognee containers via http://ollama:11434/* (internal DNS).
|
|
455
|
+
`
|
|
456
|
+
: "";
|
|
457
|
+
|
|
458
|
+
return `MISHKAN — Cognee access guide
|
|
459
|
+
Generated ${stamp}
|
|
460
|
+
Profile : ${profile.name}
|
|
461
|
+
|
|
462
|
+
This file lists every URL the Cognee stack exposes, the credentials each
|
|
463
|
+
endpoint needs, and how to reach them from a remote machine. Keep it private:
|
|
464
|
+
it carries plaintext passwords. Mode 0600. Gitignored.
|
|
465
|
+
|
|
466
|
+
${sep}
|
|
467
|
+
Cognee WORK store (per-project knowledge graph)
|
|
468
|
+
${sep}
|
|
469
|
+
|
|
470
|
+
MCP endpoint : http://127.0.0.1:7777/mcp
|
|
471
|
+
- The HTTP transport agents call to query/add knowledge.
|
|
472
|
+
- Health check: \`curl -sf http://127.0.0.1:7777/mcp\` returns 406 = healthy
|
|
473
|
+
(the endpoint requires the MCP handshake; a vanilla GET is rejected).
|
|
474
|
+
|
|
475
|
+
Cognee Graph Explorer : http://127.0.0.1:7724
|
|
476
|
+
- Web UI to browse the work graph.
|
|
477
|
+
- Login email : ${adminEmail}
|
|
478
|
+
- Login password : ${secrets.DEFAULT_USER_PASSWORD}
|
|
479
|
+
|
|
480
|
+
Cognee Backend REST : http://127.0.0.1:7737
|
|
481
|
+
- Backend API the Graph Explorer calls. Same creds as above.
|
|
482
|
+
|
|
483
|
+
Neo4j Browser : http://127.0.0.1:7716
|
|
484
|
+
- Direct cypher access to the work graph (read-only recommended).
|
|
485
|
+
- Username : ${secrets.GRAPH_DATABASE_USERNAME}
|
|
486
|
+
- Password : ${secrets.GRAPH_DATABASE_PASSWORD}
|
|
487
|
+
- Bolt URI : bolt://127.0.0.1:7709 (for desktop neo4j clients)
|
|
488
|
+
|
|
489
|
+
${sep}
|
|
490
|
+
Cognee CURATED store (cross-project reference library)
|
|
491
|
+
${sep}
|
|
492
|
+
|
|
493
|
+
MCP endpoint : http://127.0.0.1:7730/mcp
|
|
494
|
+
Cognee Graph Explorer : http://127.0.0.1:7734
|
|
495
|
+
Cognee Backend REST : http://127.0.0.1:7733
|
|
496
|
+
Neo4j Browser : http://127.0.0.1:7731
|
|
497
|
+
Neo4j Bolt : bolt://127.0.0.1:7732
|
|
498
|
+
|
|
499
|
+
Web login : ${adminEmail} / ${secrets.DEFAULT_USER_PASSWORD}
|
|
500
|
+
Neo4j login : ${secrets.GRAPH_DATABASE_USERNAME} / ${secrets.GRAPH_DATABASE_PASSWORD}
|
|
501
|
+
|
|
502
|
+
Note: the curated stack runs its own neo4j with the SAME admin password by
|
|
503
|
+
default (set by configure-knowledge). The compose stack reads .env.curated
|
|
504
|
+
for the curated containers — re-run configure-knowledge to sync both.
|
|
505
|
+
${ollamaSection}
|
|
506
|
+
${sep}
|
|
507
|
+
Reaching a remote host (SSH tunnel)
|
|
508
|
+
${sep}
|
|
509
|
+
|
|
510
|
+
If the harness runs on a remote VPS / dev server, forward the ports to your
|
|
511
|
+
laptop with one SSH session:
|
|
512
|
+
|
|
513
|
+
ssh -N -L 7777:127.0.0.1:7777 \\
|
|
514
|
+
-L 7724:127.0.0.1:7724 \\
|
|
515
|
+
-L 7716:127.0.0.1:7716 \\
|
|
516
|
+
-L 7730:127.0.0.1:7730 \\
|
|
517
|
+
-L 7734:127.0.0.1:7734 \\
|
|
518
|
+
-L 7731:127.0.0.1:7731 \\
|
|
519
|
+
<user>@<host>
|
|
520
|
+
|
|
521
|
+
Then open the URLs above on http://localhost:<port> from your laptop. The
|
|
522
|
+
ports are bound to 127.0.0.1 on the host (never 0.0.0.0), so tunneling is
|
|
523
|
+
the only remote access path by design.
|
|
524
|
+
|
|
525
|
+
${sep}
|
|
526
|
+
Postgres (internal only — not exposed)
|
|
527
|
+
${sep}
|
|
528
|
+
|
|
529
|
+
Container : mishkan-cognee-pg
|
|
530
|
+
Database : cognee
|
|
531
|
+
Password : ${secrets.DB_PASSWORD}
|
|
532
|
+
Reachable : from inside the docker network only.
|
|
533
|
+
|
|
534
|
+
To open psql for debugging:
|
|
535
|
+
docker exec -it mishkan-cognee-pg psql -U cognee -d cognee
|
|
536
|
+
(use the password above)
|
|
537
|
+
|
|
538
|
+
${sep}
|
|
539
|
+
Re-running this wizard
|
|
540
|
+
${sep}
|
|
541
|
+
|
|
542
|
+
npx mishkan-harness configure-knowledge
|
|
543
|
+
|
|
544
|
+
Re-runs preserve the three local secrets (neo4j, pg, admin) so an
|
|
545
|
+
initialised neo4j volume keeps working. Both .env and ACCESS.txt are
|
|
546
|
+
regenerated; previous .env is backed up to .env.bak.
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ─── observability opt-in (Phase 1.5 of the install contract, §10 of doc) ──
|
|
551
|
+
|
|
552
|
+
function installObservabilityStack() {
|
|
553
|
+
console.log();
|
|
554
|
+
console.log(c.bold(c.cyan("Observability stack")));
|
|
555
|
+
console.log(c.dim(
|
|
556
|
+
" Optional cross-session daemon + TUI client that aggregates the event\n" +
|
|
557
|
+
" bus into a live snapshot. Read docs/design/MISHKAN_observability.md.\n" +
|
|
558
|
+
" Requires `uv` (https://astral.sh/uv) and Python 3.11+."));
|
|
559
|
+
|
|
560
|
+
if (!commandExists("uv")) {
|
|
561
|
+
console.log(c.yellow(" uv not found — skipping observability install."));
|
|
562
|
+
console.log(c.dim(
|
|
563
|
+
" Install uv with:\n" +
|
|
564
|
+
" curl -LsSf https://astral.sh/uv/install.sh | sh\n" +
|
|
565
|
+
" Then re-run: npx mishkan-harness observability"));
|
|
566
|
+
return { installed: false, reason: "uv-missing" };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const watchdSrc = join(PKG, "payload", "mishkan", "observability", "watchd");
|
|
570
|
+
const watchSrc = join(PKG, "payload", "mishkan", "observability", "watch");
|
|
571
|
+
for (const dir of [watchdSrc, watchSrc]) {
|
|
572
|
+
if (!existsSync(dir)) {
|
|
573
|
+
warn(`observability source missing: ${tilde(dir)}`);
|
|
574
|
+
return { installed: false, reason: "payload-missing" };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
console.log(c.dim(" Installing mishkan-watchd (daemon)…"));
|
|
579
|
+
const r1 = spawnSync("uv", ["tool", "install", "--from", watchdSrc, "mishkan-watchd"],
|
|
580
|
+
{ stdio: "inherit" });
|
|
581
|
+
if (r1.status !== 0) { warn("mishkan-watchd install failed"); return { installed: false, reason: "install-failed" }; }
|
|
582
|
+
|
|
583
|
+
console.log(c.dim(" Installing mishkan-watch (TUI client)…"));
|
|
584
|
+
const r2 = spawnSync("uv", ["tool", "install", "--from", watchSrc, "mishkan-watch"],
|
|
585
|
+
{ stdio: "inherit" });
|
|
586
|
+
if (r2.status !== 0) { warn("mishkan-watch install failed"); return { installed: false, reason: "install-failed" }; }
|
|
587
|
+
|
|
588
|
+
console.log(c.green(" ✓ observability stack installed"));
|
|
589
|
+
console.log(c.dim(
|
|
590
|
+
" Open the TUI: " + c.bold("mishkan-watch") + " (auto-starts the daemon if needed)\n" +
|
|
591
|
+
" Two-terminal mode: --no-autostart on the TUI; run `mishkan-watchd start` first\n" +
|
|
592
|
+
" Stop the daemon: mishkan-watchd stop\n" +
|
|
593
|
+
" Auto-start at login: mishkan-watchd install-service"));
|
|
594
|
+
return { installed: true };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function install() {
|
|
598
|
+
const totalPhases = 7;
|
|
110
599
|
log(`installing into ${tilde(CLAUDE)} (home resolved at runtime)`);
|
|
111
600
|
ensureDir(CLAUDE);
|
|
601
|
+
|
|
602
|
+
phase(1, totalPhases, "Payload",
|
|
603
|
+
"copy agents, skills, commands, hooks, rules, cognee compose, observability sources");
|
|
112
604
|
// 1. payload/mishkan -> ~/.claude/mishkan
|
|
113
605
|
copyDir(join(PKG, "payload", "mishkan"), MISHKAN);
|
|
114
606
|
ensureDir(join(MISHKAN, "logs"));
|
|
115
607
|
ensureDir(join(MISHKAN, "cognee"));
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
//
|
|
608
|
+
|
|
609
|
+
// D-011 Phase 2: rebuild the universal skill-discovery index at install
|
|
610
|
+
// time so the router has a live index.json before the first session boots.
|
|
611
|
+
// The SessionStart hook keeps it fresh thereafter; this seeds it.
|
|
612
|
+
// Fail-open: a missing python3 or an indexer error is logged and the
|
|
613
|
+
// install continues — the router will surface `index_missing_or_unreadable`
|
|
614
|
+
// on its next call and /mishkan-skills-reindex is the recovery path.
|
|
615
|
+
const indexerPath = join(MISHKAN, "scripts", "skill-discovery-indexer.py");
|
|
616
|
+
if (existsSync(indexerPath) && commandExists("python3")) {
|
|
617
|
+
const r = spawnSync("python3", [indexerPath, "--rebuild", "--quiet"], {
|
|
618
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
619
|
+
timeout: 15000,
|
|
620
|
+
});
|
|
621
|
+
if (r.status === 0) {
|
|
622
|
+
log("seeded skill-discovery index at ~/.claude/mishkan/skill-discovery/index.json");
|
|
623
|
+
} else {
|
|
624
|
+
warn(`skill-discovery indexer exited ${r.status ?? "with error"}; install continues. ` +
|
|
625
|
+
`Run /mishkan-skills-reindex once the harness is available.`);
|
|
626
|
+
}
|
|
627
|
+
} else if (!commandExists("python3")) {
|
|
628
|
+
warn("python3 not found; skipping skill-discovery index seed. " +
|
|
629
|
+
"Install python3 then run /mishkan-skills-reindex.");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
phase(2, totalPhases, "Engineer profile",
|
|
633
|
+
"place runtime profile — never overwrites an existing edited one");
|
|
119
634
|
const realProfile = join(PKG, "docs", "engineer", "profile.md");
|
|
120
635
|
const exampleProfile = join(PKG, "docs", "engineer", "profile.example.md");
|
|
121
636
|
const runtimeProfile = join(MISHKAN, "profile.md");
|
|
@@ -126,36 +641,138 @@ function install() {
|
|
|
126
641
|
} else {
|
|
127
642
|
log("preserved existing ~/.claude/mishkan/profile.md");
|
|
128
643
|
}
|
|
129
|
-
|
|
644
|
+
|
|
645
|
+
phase(3, totalPhases, "User-level rules",
|
|
646
|
+
"refresh harness default; preserve your engineer-standards.md + CLAUDE.md");
|
|
130
647
|
ensureDir(join(CLAUDE, "rules"));
|
|
131
|
-
// y4nn-standards.md is the harness-maintained DEFAULT — always refreshed so
|
|
132
|
-
// updates flow. Customisation lives in engineer-standards.md instead.
|
|
133
648
|
copyFileSync(join(PKG, "payload", "user", "rules", "y4nn-standards.md"),
|
|
134
649
|
join(CLAUDE, "rules", "y4nn-standards.md"));
|
|
135
650
|
log("refreshed harness default ~/.claude/rules/y4nn-standards.md");
|
|
136
|
-
// engineer-standards.md is the USER's layer — placed once, never overwritten.
|
|
137
651
|
const engRule = join(CLAUDE, "rules", "engineer-standards.md");
|
|
138
652
|
if (!existsSync(engRule)) copyFileSync(join(PKG, "payload", "user", "rules", "engineer-standards.md"), engRule);
|
|
139
653
|
else log("preserved your ~/.claude/rules/engineer-standards.md");
|
|
140
|
-
// user CLAUDE.md — placed once, never overwritten.
|
|
141
654
|
const userClaude = join(CLAUDE, "CLAUDE.md");
|
|
142
655
|
if (!existsSync(userClaude)) copyFileSync(join(PKG, "payload", "user", "CLAUDE.md"), userClaude);
|
|
143
656
|
else log("preserved existing ~/.claude/CLAUDE.md");
|
|
144
|
-
|
|
657
|
+
|
|
658
|
+
phase(4, totalPhases, "Discovery symlinks",
|
|
659
|
+
"make agents, skills, commands visible to Claude Code");
|
|
145
660
|
const a = linkInto("agents", join(CLAUDE, "agents"), false);
|
|
146
661
|
const s = linkInto("skills", join(CLAUDE, "skills"), true);
|
|
147
|
-
const
|
|
148
|
-
log(`linked agents=${a.linked} (skipped ${a.skipped}), skills=${s.linked}, commands=${
|
|
149
|
-
|
|
662
|
+
const cm = linkInto("commands", join(CLAUDE, "commands"), false);
|
|
663
|
+
log(`linked agents=${a.linked} (skipped ${a.skipped}), skills=${s.linked}, commands=${cm.linked}`);
|
|
664
|
+
|
|
665
|
+
phase(5, totalPhases, "Hooks",
|
|
666
|
+
"merge MISHKAN hooks into ~/.claude/settings.json (preserves existing)");
|
|
150
667
|
mergeHooks();
|
|
151
|
-
log("hooks merged into settings.json
|
|
152
|
-
|
|
668
|
+
log("hooks merged into settings.json");
|
|
669
|
+
|
|
670
|
+
phase(6, totalPhases, "Stamp",
|
|
671
|
+
"record install version + timestamp for status / uninstall");
|
|
153
672
|
const version = JSON.parse(readFileSync(join(PKG, "package.json"), "utf8")).version;
|
|
154
673
|
writeFileSync(STAMP, JSON.stringify({ version, installedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
155
|
-
|
|
674
|
+
|
|
675
|
+
phase(7, totalPhases, "Observability (opt-in)",
|
|
676
|
+
"Python daemon + Textual TUI for live cross-session monitoring");
|
|
677
|
+
if (await promptYN(" Install observability stack now?", true)) {
|
|
678
|
+
installObservabilityStack();
|
|
679
|
+
} else {
|
|
680
|
+
console.log(c.dim(" Skipped. Re-run later: npx mishkan-harness observability"));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Auto-symlink ~/.local/bin/mishkan -> this package's bin/mishkan.js so
|
|
684
|
+
// the user gets `mishkan <subcommand>` directly without `npx` or a
|
|
685
|
+
// global npm install. Skipped (silently) when:
|
|
686
|
+
// - ~/.local/bin doesn't exist (user didn't opt into local bins)
|
|
687
|
+
// - the link already exists pointing to a different mishkan install
|
|
688
|
+
// (don't clobber another version)
|
|
689
|
+
// - the link exists pointing to us (idempotent re-run)
|
|
690
|
+
const localBin = join(HOME, ".local", "bin");
|
|
691
|
+
const linkTarget = join(PKG, "bin", "mishkan.js");
|
|
692
|
+
const linkPath = join(localBin, "mishkan");
|
|
693
|
+
let directAccess = "absent";
|
|
694
|
+
if (existsSync(localBin)) {
|
|
695
|
+
if (existsSync(linkPath) || isSymlink(linkPath)) {
|
|
696
|
+
try {
|
|
697
|
+
const current = isSymlink(linkPath) ? readlinkSync(linkPath) : null;
|
|
698
|
+
if (current === linkTarget) {
|
|
699
|
+
directAccess = "linked";
|
|
700
|
+
} else {
|
|
701
|
+
directAccess = "blocked"; // a different file lives at the path
|
|
702
|
+
}
|
|
703
|
+
} catch { directAccess = "blocked"; }
|
|
704
|
+
} else {
|
|
705
|
+
try {
|
|
706
|
+
// Defensive +x — npm publish normally sets this from the package.json
|
|
707
|
+
// `bin` field, but a checkout-from-source flow may not.
|
|
708
|
+
try { const st = statSync(linkTarget); chmodSync(linkTarget, st.mode | 0o111); } catch {}
|
|
709
|
+
symlinkSync(linkTarget, linkPath);
|
|
710
|
+
directAccess = "linked";
|
|
711
|
+
log(`linked ${tilde(linkPath)} -> ${tilde(linkTarget)}`);
|
|
712
|
+
} catch (e) {
|
|
713
|
+
directAccess = "failed";
|
|
714
|
+
warn(`could not symlink ${tilde(linkPath)}: ${e.message}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const pathHasLocalBin = (process.env.PATH || "").split(":").includes(localBin);
|
|
719
|
+
|
|
720
|
+
console.log();
|
|
721
|
+
console.log(c.green(`✓ MISHKAN v${version} installed.`));
|
|
722
|
+
if (directAccess === "linked" && pathHasLocalBin) {
|
|
723
|
+
console.log(c.dim(
|
|
724
|
+
" Run a Claude session and talk to Nehemiah, or /mishkan-init in a project.\n" +
|
|
725
|
+
"\n" +
|
|
726
|
+
" Commands available directly on your PATH (no `npx` needed):\n" +
|
|
727
|
+
" mishkan configure-knowledge wizard: LLM provider + cognee secrets\n" +
|
|
728
|
+
" mishkan code-graph status inspect the project's Graphify graph\n" +
|
|
729
|
+
" mishkan org print the 45-agent reference\n" +
|
|
730
|
+
" mishkan status show install state\n" +
|
|
731
|
+
" mishkan-watch live observability TUI (auto-starts daemon)"));
|
|
732
|
+
} else if (directAccess === "linked") {
|
|
733
|
+
console.log(c.dim(
|
|
734
|
+
" Run a Claude session and talk to Nehemiah, or /mishkan-init in a project.\n" +
|
|
735
|
+
"\n" +
|
|
736
|
+
` Symlinked ${tilde(linkPath)} — add it to PATH for direct access:\n` +
|
|
737
|
+
` echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # or ~/.zshrc\n` +
|
|
738
|
+
" Until then, use `npx mishkan-harness <subcommand>`."));
|
|
739
|
+
} else {
|
|
740
|
+
console.log(c.dim(
|
|
741
|
+
" Run a Claude session and talk to Nehemiah, or /mishkan-init in a project.\n" +
|
|
742
|
+
"\n" +
|
|
743
|
+
" Day-to-day commands (via `npx mishkan-harness …`):\n" +
|
|
744
|
+
" npx mishkan-harness configure-knowledge wizard: LLM provider + cognee secrets\n" +
|
|
745
|
+
" npx mishkan-harness code-graph status inspect the project's Graphify graph\n" +
|
|
746
|
+
" npx mishkan-harness org print the 45-agent reference\n" +
|
|
747
|
+
" mishkan-watch live observability TUI (auto-starts daemon)\n" +
|
|
748
|
+
"\n" +
|
|
749
|
+
" Want direct access? Either:\n" +
|
|
750
|
+
" npm install -g mishkan-harness # global npm bin\n" +
|
|
751
|
+
" mkdir -p ~/.local/bin && ln -sf " + linkTarget + " ~/.local/bin/mishkan # symlink"));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function uninstallObservabilityHint() {
|
|
756
|
+
if (commandExists("uv")) {
|
|
757
|
+
console.log(c.dim(
|
|
758
|
+
" Observability stack installed via uv tool — remove manually if desired:\n" +
|
|
759
|
+
" uv tool uninstall mishkan-watch mishkan-watchd"));
|
|
760
|
+
}
|
|
156
761
|
}
|
|
157
762
|
|
|
158
763
|
function uninstall({ purge = false } = {}) {
|
|
764
|
+
// remove the ~/.local/bin/mishkan symlink if we created it (it points
|
|
765
|
+
// at this package's bin/mishkan.js).
|
|
766
|
+
const localLink = join(HOME, ".local", "bin", "mishkan");
|
|
767
|
+
if (isSymlink(localLink)) {
|
|
768
|
+
try {
|
|
769
|
+
const t = readlinkSync(localLink);
|
|
770
|
+
if (t.includes("/bin/mishkan.js")) {
|
|
771
|
+
rmSync(localLink);
|
|
772
|
+
log(`removed ${tilde(localLink)}`);
|
|
773
|
+
}
|
|
774
|
+
} catch { /* ignore */ }
|
|
775
|
+
}
|
|
159
776
|
// remove symlinks that point into mishkan
|
|
160
777
|
for (const [sub, dirEntries] of [["agents", false], ["commands", false], ["skills", true]]) {
|
|
161
778
|
const dir = join(CLAUDE, sub);
|
|
@@ -180,6 +797,7 @@ function uninstall({ purge = false } = {}) {
|
|
|
180
797
|
} else {
|
|
181
798
|
log("kept user-level CLAUDE.md, y4nn-standards.md, and engineer-standards.md (use --purge to remove the default).");
|
|
182
799
|
}
|
|
800
|
+
uninstallObservabilityHint();
|
|
183
801
|
}
|
|
184
802
|
|
|
185
803
|
function status() {
|
|
@@ -205,17 +823,183 @@ function status() {
|
|
|
205
823
|
log(`cognee dir: ${existsSync(join(MISHKAN,"cognee")) ? "present (deploy the container to activate the graph)" : "missing"}`);
|
|
206
824
|
}
|
|
207
825
|
|
|
826
|
+
// ─── org reference ─────────────────────────────────────────────────────────
|
|
827
|
+
// ─── code-graph (Graphify) inspection ──────────────────────────────────────
|
|
828
|
+
function codeGraphCmd(argv) {
|
|
829
|
+
const sub = argv[0] || "status";
|
|
830
|
+
const cwd = process.cwd();
|
|
831
|
+
const outDir = join(cwd, "graphify-out");
|
|
832
|
+
if (!existsSync(outDir)) {
|
|
833
|
+
console.error(c.red("no code-graph found in this project."));
|
|
834
|
+
console.log("run `graphify update .` from the project root, then retry.");
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
const graphJson = join(outDir, "graph.json");
|
|
838
|
+
const graphHtml = join(outDir, "graph.html");
|
|
839
|
+
if (sub === "status") {
|
|
840
|
+
let nodes = 0, edges = 0, lastScan = "?";
|
|
841
|
+
if (existsSync(graphJson)) {
|
|
842
|
+
try {
|
|
843
|
+
const g = JSON.parse(readFileSync(graphJson, "utf8"));
|
|
844
|
+
nodes = (g.nodes || []).length;
|
|
845
|
+
edges = (g.links || g.edges || []).length;
|
|
846
|
+
lastScan = statSync(graphJson).mtime.toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
847
|
+
} catch {}
|
|
848
|
+
}
|
|
849
|
+
console.log("");
|
|
850
|
+
console.log(" " + c.bold("code-graph") + " " + c.dim(tilde(cwd)));
|
|
851
|
+
console.log("");
|
|
852
|
+
console.log(" nodes " + c.cyan(String(nodes)));
|
|
853
|
+
console.log(" edges " + c.cyan(String(edges)));
|
|
854
|
+
console.log(" last scan " + c.dim(lastScan));
|
|
855
|
+
console.log("");
|
|
856
|
+
if (existsSync(graphHtml)) {
|
|
857
|
+
console.log(" " + c.dim("open visualisation: ") + c.bold("npx mishkan-harness code-graph open"));
|
|
858
|
+
}
|
|
859
|
+
console.log(" " + c.dim("refresh: ") + c.bold("npx mishkan-harness code-graph scan"));
|
|
860
|
+
console.log("");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (sub === "open") {
|
|
864
|
+
if (!existsSync(graphHtml)) {
|
|
865
|
+
console.error("graph.html missing. run `npx mishkan-harness code-graph scan` first.");
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
869
|
+
const r = spawnSync(opener, [graphHtml], { stdio: "ignore" });
|
|
870
|
+
if (r.status !== 0) {
|
|
871
|
+
console.log("could not auto-open. visualisation at:");
|
|
872
|
+
console.log(" file://" + graphHtml);
|
|
873
|
+
} else {
|
|
874
|
+
console.log(c.green("opened:") + " " + tilde(graphHtml));
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (sub === "scan") {
|
|
879
|
+
const r = spawnSync("graphify", ["update", "."], { stdio: "inherit", cwd });
|
|
880
|
+
process.exit(r.status || 0);
|
|
881
|
+
}
|
|
882
|
+
console.error("unknown subcommand: " + sub);
|
|
883
|
+
console.log("use one of: status | open | scan");
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function printOrgRef({ json = false } = {}) {
|
|
888
|
+
const candidates = [
|
|
889
|
+
join(MISHKAN, "org", "org.json"),
|
|
890
|
+
join(PKG, "payload", "mishkan", "org", "org.json"),
|
|
891
|
+
];
|
|
892
|
+
let data = null;
|
|
893
|
+
for (const p of candidates) {
|
|
894
|
+
if (existsSync(p)) {
|
|
895
|
+
try { data = JSON.parse(readFileSync(p, "utf8")); break; } catch {}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (!data) { console.error("org.json not found. Run `npx mishkan-harness install` first."); process.exit(2); }
|
|
899
|
+
if (json) { console.log(JSON.stringify(data, null, 2)); return; }
|
|
900
|
+
console.log(`\n${c.bold("MISHKAN")} — 45 agents across 8 groups\n`);
|
|
901
|
+
for (const grp of data.groups || []) {
|
|
902
|
+
const dom = grp.domain ? c.cyan(` · ${grp.domain}`) : "";
|
|
903
|
+
console.log(c.bold(grp.label) + dom + c.dim(` (${(grp.agents||[]).length})`));
|
|
904
|
+
if (grp.hebrew) {
|
|
905
|
+
const meaning = grp.hebrew_meaning ? c.dim(` — ${grp.hebrew_meaning}`) : "";
|
|
906
|
+
console.log(" " + c.cyan(grp.hebrew) + meaning);
|
|
907
|
+
}
|
|
908
|
+
if (grp.mission) console.log(" " + c.dim("mission ") + grp.mission);
|
|
909
|
+
if (grp.charter) console.log(" " + c.dim("charter ") + grp.charter);
|
|
910
|
+
if (grp.relationships) console.log(" " + c.dim("links ") + grp.relationships);
|
|
911
|
+
if (grp.mission || grp.charter || grp.relationships) console.log("");
|
|
912
|
+
for (const ag of grp.agents || []) {
|
|
913
|
+
const alias = ag.alias.charAt(0).toUpperCase() + ag.alias.slice(1);
|
|
914
|
+
console.log(" " + c.bold(alias.padEnd(14)) + c.dim("· ") + ag.role);
|
|
915
|
+
if (ag.description) console.log(" " + c.dim(ag.description));
|
|
916
|
+
}
|
|
917
|
+
console.log("");
|
|
918
|
+
}
|
|
919
|
+
console.log(c.dim(`source: ${data.generated_from || "docs/design/MISHKAN_agent_aliases.md"}`));
|
|
920
|
+
}
|
|
921
|
+
|
|
208
922
|
const cmd = process.argv[2];
|
|
209
923
|
const flags = new Set(process.argv.slice(3));
|
|
210
924
|
switch (cmd) {
|
|
211
|
-
case "install": install(); break;
|
|
925
|
+
case "install": await install(); break;
|
|
212
926
|
case "uninstall": uninstall({ purge: flags.has("--purge") }); break;
|
|
213
927
|
case "status": status(); break;
|
|
928
|
+
case "observability": installObservabilityStack(); break;
|
|
929
|
+
case "configure-knowledge": await configureKnowledge(); break;
|
|
930
|
+
case "org": case "org-ref": printOrgRef({ json: flags.has("--json") }); break;
|
|
931
|
+
case "code-graph": codeGraphCmd(process.argv.slice(3)); break;
|
|
932
|
+
case "help": case "--help": case "-h":
|
|
933
|
+
printHelp(); break;
|
|
214
934
|
default:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
935
|
+
printHelp(); break;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Help / usage — always shows. Detects whether ~/.local/bin/mishkan
|
|
939
|
+
// symlinks back to us; when yes we print the short `mishkan <subcommand>`
|
|
940
|
+
// form because that's what the user can actually type. Otherwise we
|
|
941
|
+
// print the `npx mishkan-harness <subcommand>` form so copy-paste works
|
|
942
|
+
// on a fresh shell without the symlink.
|
|
943
|
+
function printHelp() {
|
|
944
|
+
const link = join(HOME, ".local", "bin", "mishkan");
|
|
945
|
+
let direct = false;
|
|
946
|
+
if (isSymlink(link)) {
|
|
947
|
+
try {
|
|
948
|
+
const t = readlinkSync(link);
|
|
949
|
+
direct = t === join(PKG, "bin", "mishkan.js");
|
|
950
|
+
} catch { direct = false; }
|
|
951
|
+
}
|
|
952
|
+
const prefix = direct ? "mishkan" : "npx mishkan-harness";
|
|
953
|
+
const pkgVersion = (() => {
|
|
954
|
+
try { return JSON.parse(readFileSync(join(PKG, "package.json"), "utf8")).version; } catch { return "?"; }
|
|
955
|
+
})();
|
|
956
|
+
console.log("");
|
|
957
|
+
console.log(c.bold(`MISHKAN harness v${pkgVersion}`));
|
|
958
|
+
console.log("");
|
|
959
|
+
if (direct) {
|
|
960
|
+
console.log(c.dim(" Direct access detected: ~/.local/bin/mishkan -> bin/mishkan.js"));
|
|
961
|
+
console.log(c.dim(" All commands below run directly without `npx`."));
|
|
962
|
+
} else {
|
|
963
|
+
console.log(c.dim(" No ~/.local/bin/mishkan symlink — commands run via `npx mishkan-harness …`."));
|
|
964
|
+
console.log(c.dim(" After `install` the harness can symlink for direct access automatically."));
|
|
965
|
+
}
|
|
966
|
+
console.log("");
|
|
967
|
+
console.log(c.bold("Install / update"));
|
|
968
|
+
console.log(" " + c.bold(`${prefix} install`) + " Install/refresh into ~/.claude (idempotent)");
|
|
969
|
+
console.log(" " + c.bold(`${prefix} observability`) + " Install only the observability stack (daemon + TUI, needs uv)");
|
|
970
|
+
console.log(" " + c.bold(`${prefix} uninstall`) + " Remove harness (keeps your CLAUDE.md and rules)");
|
|
971
|
+
console.log(" " + c.bold(`${prefix} uninstall --purge`) + " Also remove the user-level y4nn-standards.md");
|
|
972
|
+
console.log("");
|
|
973
|
+
console.log(c.bold("Configure"));
|
|
974
|
+
console.log(" " + c.bold(`${prefix} configure-knowledge`) + " Wizard: LLM provider + cognee .env (neo4j/pg/admin secrets)");
|
|
975
|
+
console.log("");
|
|
976
|
+
console.log(c.bold("Inspect"));
|
|
977
|
+
console.log(" " + c.bold(`${prefix} status`) + " Show install state, runtime profile, cognee dir, version stamp");
|
|
978
|
+
console.log(" " + c.bold(`${prefix} org [--json]`) + " Print the 45-agent organisation reference (teams + roles + descriptions)");
|
|
979
|
+
console.log(" " + c.bold(`${prefix} code-graph [sub]`) + " Inspect the project's code-graph (Graphify): status | open | scan");
|
|
980
|
+
console.log("");
|
|
981
|
+
console.log(c.bold("Run the live observability TUI"));
|
|
982
|
+
console.log(" " + c.bold("mishkan-watch") + " Open the TUI; auto-starts the daemon if needed");
|
|
983
|
+
console.log(" " + c.bold("mishkan-watch --no-autostart") + " Refuse to fork the daemon; you start it yourself");
|
|
984
|
+
console.log(" " + c.bold("mishkan-watchd start") + " | " + c.bold("stop") + " | " + c.bold("status") + " Daemon lifecycle, when you want manual control");
|
|
985
|
+
console.log("");
|
|
986
|
+
console.log(c.bold("Inside a Claude Code session"));
|
|
987
|
+
console.log(" " + c.dim("Talk to Nehemiah (PM) in plain language — exploration mode is the default."));
|
|
988
|
+
console.log(" " + c.dim("Slash commands available after install:"));
|
|
989
|
+
console.log(" " + c.bold("/mishkan-init") + " Spec chain on a new project (PRD → SRS → CONTRACT → …)");
|
|
990
|
+
console.log(" " + c.bold("/mishkan-resume") + " Resume sprint state + open blockers");
|
|
991
|
+
console.log(" " + c.bold("/sprint-close") + " Reporters → aggregate → docs pull → graph promote");
|
|
992
|
+
console.log(" " + c.bold("/code-graph") + " status|open|scan" + " Inspect / open / refresh the Graphify graph");
|
|
993
|
+
console.log(" " + c.bold("/skills") + " <task description> Skill-discovery router — 3-bucket result");
|
|
994
|
+
console.log(" " + c.bold("/mishkan-skills-reindex") + " Rebuild the universal skill index");
|
|
995
|
+
console.log(" " + c.bold("/mishkan-skills-misses") + " Aggregate miss-log signal for skill-discovery tuning");
|
|
996
|
+
console.log(" " + c.bold("/mishkan-org-reference") + " Print the 45-agent reference inline");
|
|
997
|
+
console.log(" " + c.bold("/eval-baruch") + " Run the Baruch contract eval (schema + golden case)");
|
|
998
|
+
console.log(" " + c.bold("/dep-audit") + " Cross-project dependency + supply-chain audit");
|
|
999
|
+
console.log(" " + c.bold("/promote") + " Promote a learning into Cognee by blast radius");
|
|
1000
|
+
console.log(" " + c.bold("/sefer-pull") + " Trigger a documentation pull");
|
|
1001
|
+
console.log("");
|
|
1002
|
+
console.log(c.dim(" Docs: docs/usage/ · Decisions: docs/design/MISHKAN_decisions.md"));
|
|
1003
|
+
console.log(c.dim(" Repo: https://github.com/Y4NN777/mishkan-cc-harness"));
|
|
1004
|
+
console.log("");
|
|
221
1005
|
}
|