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.
Files changed (139) hide show
  1. package/README.md +121 -134
  2. package/bin/mishkan.js +810 -26
  3. package/docs/design/MISHKAN_decisions.md +842 -0
  4. package/docs/design/MISHKAN_harness_design.md +1 -33
  5. package/docs/design/MISHKAN_observability.md +1027 -0
  6. package/docs/research/graphify-token-saving-poc.md +108 -0
  7. package/docs/usage/01-installation.md +40 -12
  8. package/docs/usage/09-workflows.md +74 -4
  9. package/docs/usage/10-observability.md +173 -0
  10. package/docs/usage/11-graphify.md +264 -0
  11. package/docs/usage/12-skill-discovery.md +294 -0
  12. package/docs/usage/README.md +5 -2
  13. package/package.json +1 -1
  14. package/payload/install/settings.hooks.json +33 -0
  15. package/payload/mishkan/AGENT_SPEC.md +25 -7
  16. package/payload/mishkan/agents/ahikam.md +4 -2
  17. package/payload/mishkan/agents/aholiab.md +4 -2
  18. package/payload/mishkan/agents/asaph.md +5 -2
  19. package/payload/mishkan/agents/baruch.md +19 -2
  20. package/payload/mishkan/agents/benaiah.md +4 -2
  21. package/payload/mishkan/agents/bezalel.md +4 -2
  22. package/payload/mishkan/agents/caleb.md +4 -2
  23. package/payload/mishkan/agents/deborah.md +4 -2
  24. package/payload/mishkan/agents/elasah.md +4 -2
  25. package/payload/mishkan/agents/eliashib.md +4 -2
  26. package/payload/mishkan/agents/ezra.md +4 -2
  27. package/payload/mishkan/agents/hanun.md +5 -2
  28. package/payload/mishkan/agents/hiram.md +5 -2
  29. package/payload/mishkan/agents/hizkiah.md +5 -2
  30. package/payload/mishkan/agents/huldah.md +4 -2
  31. package/payload/mishkan/agents/huram.md +4 -2
  32. package/payload/mishkan/agents/hushai.md +5 -2
  33. package/payload/mishkan/agents/igal.md +4 -2
  34. package/payload/mishkan/agents/ira.md +5 -2
  35. package/payload/mishkan/agents/jahaziel.md +5 -2
  36. package/payload/mishkan/agents/jakin.md +4 -2
  37. package/payload/mishkan/agents/jehonathan.md +5 -2
  38. package/payload/mishkan/agents/jehoshaphat.md +4 -2
  39. package/payload/mishkan/agents/joab.md +5 -2
  40. package/payload/mishkan/agents/joah.md +5 -2
  41. package/payload/mishkan/agents/maaseiah.md +4 -2
  42. package/payload/mishkan/agents/meremoth.md +5 -2
  43. package/payload/mishkan/agents/meshullam.md +5 -2
  44. package/payload/mishkan/agents/nathan.md +5 -2
  45. package/payload/mishkan/agents/nehemiah.md +4 -2
  46. package/payload/mishkan/agents/obed.md +4 -2
  47. package/payload/mishkan/agents/oholiab.md +5 -2
  48. package/payload/mishkan/agents/palal.md +5 -2
  49. package/payload/mishkan/agents/phinehas.md +4 -2
  50. package/payload/mishkan/agents/rehum.md +4 -2
  51. package/payload/mishkan/agents/salma.md +5 -2
  52. package/payload/mishkan/agents/seraiah.md +4 -2
  53. package/payload/mishkan/agents/shallum.md +5 -2
  54. package/payload/mishkan/agents/shaphan.md +4 -2
  55. package/payload/mishkan/agents/shemaiah.md +4 -2
  56. package/payload/mishkan/agents/shevna.md +5 -2
  57. package/payload/mishkan/agents/uriah.md +5 -2
  58. package/payload/mishkan/agents/zaccur.md +4 -2
  59. package/payload/mishkan/agents/zadok.md +5 -2
  60. package/payload/mishkan/agents/zerubbabel.md +4 -2
  61. package/payload/mishkan/commands/code-graph.md +35 -0
  62. package/payload/mishkan/commands/eval-baruch.md +57 -0
  63. package/payload/mishkan/commands/mishkan-org-reference.md +58 -0
  64. package/payload/mishkan/commands/mishkan-skills-misses.md +47 -0
  65. package/payload/mishkan/commands/mishkan-skills-reindex.md +33 -0
  66. package/payload/mishkan/commands/skills.md +35 -0
  67. package/payload/mishkan/commands/sprint-close.md +3 -1
  68. package/payload/mishkan/hooks/model-route.py +32 -0
  69. package/payload/mishkan/hooks/post-tool-observe.sh +222 -14
  70. package/payload/mishkan/hooks/pre-tool-knowledge-route.sh +231 -0
  71. package/payload/mishkan/hooks/pre-tool-security.sh +16 -0
  72. package/payload/mishkan/hooks/pre-tool-task-skill-route.sh +122 -0
  73. package/payload/mishkan/hooks/pre-tool-trace.sh +43 -0
  74. package/payload/mishkan/hooks/session-start-skill-index.sh +35 -0
  75. package/payload/mishkan/observability/README.md +100 -0
  76. package/payload/mishkan/observability/bus.py +89 -0
  77. package/payload/mishkan/observability/bus.sh +117 -0
  78. package/payload/mishkan/observability/schema.json +209 -0
  79. package/payload/mishkan/observability/usage_parser.py +211 -0
  80. package/payload/mishkan/observability/watch/README.md +62 -0
  81. package/payload/mishkan/observability/watch/pyproject.toml +22 -0
  82. package/payload/mishkan/observability/watch/src/mishkan_watch/__init__.py +3 -0
  83. package/payload/mishkan/observability/watch/src/mishkan_watch/__main__.py +111 -0
  84. package/payload/mishkan/observability/watch/src/mishkan_watch/app.py +296 -0
  85. package/payload/mishkan/observability/watch/src/mishkan_watch/client.py +134 -0
  86. package/payload/mishkan/observability/watch/src/mishkan_watch/org_data.py +79 -0
  87. package/payload/mishkan/observability/watch/src/mishkan_watch/skills_data.py +267 -0
  88. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/__init__.py +1 -0
  89. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/activity.py +100 -0
  90. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/agents.py +310 -0
  91. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/knowledge.py +297 -0
  92. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/live.py +407 -0
  93. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/org.py +139 -0
  94. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/skills.py +187 -0
  95. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/usage.py +377 -0
  96. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/workflows.py +361 -0
  97. package/payload/mishkan/observability/watch/src/mishkan_watch/theme.tcss +328 -0
  98. package/payload/mishkan/observability/watchd/README.md +59 -0
  99. package/payload/mishkan/observability/watchd/pyproject.toml +23 -0
  100. package/payload/mishkan/observability/watchd/src/mishkan_watchd/__init__.py +3 -0
  101. package/payload/mishkan/observability/watchd/src/mishkan_watchd/__main__.py +260 -0
  102. package/payload/mishkan/observability/watchd/src/mishkan_watchd/lifecycle.py +68 -0
  103. package/payload/mishkan/observability/watchd/src/mishkan_watchd/server.py +134 -0
  104. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/__init__.py +6 -0
  105. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/bus_tail.py +110 -0
  106. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/cognee_poll.py +228 -0
  107. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/graphify_tail.py +220 -0
  108. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/mcp_probe.py +230 -0
  109. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/session_discover.py +74 -0
  110. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/session_tail.py +161 -0
  111. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/subagent_tail.py +195 -0
  112. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/worktree_poll.py +118 -0
  113. package/payload/mishkan/observability/watchd/src/mishkan_watchd/state.py +442 -0
  114. package/payload/mishkan/observability/watchd/tests/test_state.py +81 -0
  115. package/payload/mishkan/org/org.json +141 -0
  116. package/payload/mishkan/scripts/pre-commit-eval-gate.sh +50 -0
  117. package/payload/mishkan/scripts/skill-discovery-indexer.py +462 -0
  118. package/payload/mishkan/scripts/skill-discovery-misses.py +173 -0
  119. package/payload/mishkan/scripts/skill-discovery-router.py +457 -0
  120. package/payload/mishkan/scripts/skill-discovery-smoke.sh +156 -0
  121. package/payload/mishkan/skills/graphify-query-craft/SKILL.md +162 -0
  122. package/payload/mishkan/skills/mishkan-init/SKILL.md +15 -0
  123. package/payload/mishkan/skills/skill-discovery/SKILL.md +120 -0
  124. package/payload/mishkan/templates/case-node.schema.json +1 -0
  125. package/payload/mishkan/templates/observability-log.schema.json +1 -0
  126. package/payload/mishkan/workflows/README.md +78 -13
  127. package/payload/mishkan/workflows/chosheb-feature-ship.js +75 -0
  128. package/payload/mishkan/workflows/migdal-dr-drill.js +94 -0
  129. package/payload/mishkan/workflows/migdal-infra-change.js +67 -0
  130. package/payload/mishkan/workflows/mishkan-blast-radius.js +144 -0
  131. package/payload/mishkan/workflows/mishkan-init.js +58 -0
  132. package/payload/mishkan/workflows/mishkan-knowledge-gap-discovery.js +121 -0
  133. package/payload/mishkan/workflows/mishkan-standards-rollout.js +153 -0
  134. package/payload/mishkan/workflows/mishmar-security-gate.js +83 -0
  135. package/payload/mishkan/workflows/panim-ds-rollout.js +83 -0
  136. package/payload/mishkan/workflows/sefer-release-notes.js +85 -0
  137. package/payload/mishkan/workflows/yasad-data-migration-wave.js +78 -0
  138. package/payload/mishkan/workflows/yasad-schema-evolution.js +76 -0
  139. 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 } from "node:fs";
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
- function install() {
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
- // 2. engineer profile -> runtime. Prefer a real (gitignored) profile.md if the
117
- // engineer made one; otherwise ship the sanitized example. Never overwrite an
118
- // existing runtime profile the engineer may have edited in place.
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
- // 3. user-level files
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
- // 4. symlinks for discovery
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 c = linkInto("commands", join(CLAUDE, "commands"), false);
148
- log(`linked agents=${a.linked} (skipped ${a.skipped}), skills=${s.linked}, commands=${c.linked}`);
149
- // 5. hooks
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 (existing hooks preserved)");
152
- // 6. stamp
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
- log(`installed v${version}. Run a Claude session and talk to Nehemiah, or /mishkan-init in a project.`);
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
- console.log(`MISHKAN harness installer
216
- Usage:
217
- npx mishkan-harness install Install/refresh into ~/.claude (idempotent)
218
- npx mishkan-harness status Show install state
219
- npx mishkan-harness uninstall Remove harness (keeps your CLAUDE.md & rules)
220
- npx mishkan-harness uninstall --purge Also remove user-level rule`);
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
  }