mishkan-harness 0.2.5 → 0.2.7

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 (145) hide show
  1. package/README.md +49 -11
  2. package/bin/mishkan.js +562 -79
  3. package/docs/design/MISHKAN_decisions.md +398 -8
  4. package/docs/design/MISHKAN_observability.md +8 -8
  5. package/docs/engineer/profile-readable.md +201 -0
  6. package/docs/engineer/profile.md +754 -0
  7. package/docs/usage/01-installation.md +22 -8
  8. package/docs/usage/02-project-init.md +30 -13
  9. package/docs/usage/03-orchestration.md +46 -7
  10. package/docs/usage/04-memory-layer.md +135 -69
  11. package/docs/usage/06-llm-providers.md +2 -1
  12. package/docs/usage/07-troubleshooting.md +23 -0
  13. package/docs/usage/08-glossary.md +12 -3
  14. package/docs/usage/09-workflows.md +15 -3
  15. package/docs/usage/10-observability.md +3 -3
  16. package/docs/usage/11-graphify.md +38 -18
  17. package/docs/usage/12-cli.md +87 -0
  18. package/docs/usage/12-skill-discovery.md +11 -0
  19. package/docs/usage/README.md +35 -19
  20. package/package.json +1 -1
  21. package/payload/mishkan/AGENT_SPEC.md +3 -1
  22. package/payload/mishkan/agents/ahikam.md +3 -1
  23. package/payload/mishkan/agents/aholiab.md +3 -1
  24. package/payload/mishkan/agents/asaph.md +3 -1
  25. package/payload/mishkan/agents/baruch.md +8 -1
  26. package/payload/mishkan/agents/benaiah.md +3 -1
  27. package/payload/mishkan/agents/bezalel.md +3 -1
  28. package/payload/mishkan/agents/caleb.md +3 -1
  29. package/payload/mishkan/agents/deborah.md +3 -1
  30. package/payload/mishkan/agents/elasah.md +3 -1
  31. package/payload/mishkan/agents/eliashib.md +3 -1
  32. package/payload/mishkan/agents/ezra.md +3 -1
  33. package/payload/mishkan/agents/hanun.md +3 -1
  34. package/payload/mishkan/agents/hiram.md +3 -1
  35. package/payload/mishkan/agents/hizkiah.md +3 -1
  36. package/payload/mishkan/agents/huldah.md +3 -1
  37. package/payload/mishkan/agents/huram.md +3 -1
  38. package/payload/mishkan/agents/hushai.md +3 -1
  39. package/payload/mishkan/agents/igal.md +3 -1
  40. package/payload/mishkan/agents/ira.md +3 -1
  41. package/payload/mishkan/agents/jahaziel.md +3 -1
  42. package/payload/mishkan/agents/jakin.md +3 -1
  43. package/payload/mishkan/agents/jehonathan.md +3 -1
  44. package/payload/mishkan/agents/jehoshaphat.md +3 -1
  45. package/payload/mishkan/agents/joab.md +3 -1
  46. package/payload/mishkan/agents/joah.md +3 -1
  47. package/payload/mishkan/agents/maaseiah.md +3 -1
  48. package/payload/mishkan/agents/meremoth.md +3 -1
  49. package/payload/mishkan/agents/meshullam.md +3 -1
  50. package/payload/mishkan/agents/nathan.md +3 -1
  51. package/payload/mishkan/agents/nehemiah.md +3 -1
  52. package/payload/mishkan/agents/obed.md +3 -1
  53. package/payload/mishkan/agents/oholiab.md +3 -1
  54. package/payload/mishkan/agents/palal.md +3 -1
  55. package/payload/mishkan/agents/phinehas.md +3 -1
  56. package/payload/mishkan/agents/rehum.md +3 -1
  57. package/payload/mishkan/agents/salma.md +3 -1
  58. package/payload/mishkan/agents/seraiah.md +3 -1
  59. package/payload/mishkan/agents/shallum.md +3 -1
  60. package/payload/mishkan/agents/shaphan.md +3 -1
  61. package/payload/mishkan/agents/shemaiah.md +6 -1
  62. package/payload/mishkan/agents/shevna.md +3 -1
  63. package/payload/mishkan/agents/uriah.md +3 -1
  64. package/payload/mishkan/agents/zaccur.md +3 -1
  65. package/payload/mishkan/agents/zadok.md +3 -1
  66. package/payload/mishkan/agents/zerubbabel.md +3 -1
  67. package/payload/mishkan/cognee/.env.example +11 -0
  68. package/payload/mishkan/cognee/Dockerfile +20 -0
  69. package/payload/mishkan/cognee/README.md +34 -14
  70. package/payload/mishkan/cognee/docker-compose.curated.yml +33 -6
  71. package/payload/mishkan/cognee/docker-compose.selfhosted.yml +35 -5
  72. package/payload/mishkan/cognee/docker-compose.work.yml +191 -0
  73. package/payload/mishkan/cognee/docker-compose.yml +18 -3
  74. package/payload/mishkan/cognee/patches/cognee-mcp-core-align.py +72 -0
  75. package/payload/mishkan/cognee/patches/cognee-mcp-recall-user.py +82 -0
  76. package/payload/mishkan/cognee/promote-curated.py +77 -0
  77. package/payload/mishkan/cognee/prune-store.py +23 -0
  78. package/payload/mishkan/commands/mishkan-init.md +4 -1
  79. package/payload/mishkan/commands/mishkan-org-reference.md +2 -2
  80. package/payload/mishkan/config/curated-library.yaml +1 -0
  81. package/payload/mishkan/config/model-routing.yaml +23 -14
  82. package/payload/mishkan/evals/baruch/fixtures/invalid/bad-curated-candidate.json +22 -0
  83. package/payload/mishkan/evals/baruch/fixtures/valid/resolved-curated-candidate.json +23 -0
  84. package/payload/mishkan/hooks/model-route.py +14 -1
  85. package/payload/mishkan/hooks/post-tool-observe.sh +55 -7
  86. package/payload/mishkan/hooks/pre-tool-trace.sh +40 -6
  87. package/payload/mishkan/observability/README.md +15 -12
  88. package/payload/mishkan/observability/schema.json +18 -1
  89. package/payload/mishkan/observability/usage_parser.py +1 -0
  90. package/payload/mishkan/observability/watch/pyproject.toml +1 -1
  91. package/payload/mishkan/observability/watch/src/mishkan_watch/__init__.py +1 -1
  92. package/payload/mishkan/observability/watch/src/mishkan_watch/__main__.py +78 -21
  93. package/payload/mishkan/observability/watch/src/mishkan_watch/app.py +56 -3
  94. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/agents.py +29 -7
  95. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/knowledge.py +83 -8
  96. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/live.py +42 -13
  97. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/usage.py +10 -3
  98. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/workflows.py +47 -7
  99. package/payload/mishkan/observability/watchd/pyproject.toml +1 -1
  100. package/payload/mishkan/observability/watchd/src/mishkan_watchd/__init__.py +1 -1
  101. package/payload/mishkan/observability/watchd/src/mishkan_watchd/__main__.py +87 -11
  102. package/payload/mishkan/observability/watchd/src/mishkan_watchd/server.py +126 -9
  103. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/cognee_poll.py +136 -49
  104. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/graphify_tail.py +38 -64
  105. package/payload/mishkan/observability/watchd/src/mishkan_watchd/state.py +222 -34
  106. package/payload/mishkan/observability/watchd/tests/test_state.py +502 -4
  107. package/payload/mishkan/ontology.md +8 -1
  108. package/payload/mishkan/ontology.ttl +188 -0
  109. package/payload/mishkan/scripts/ensure-work-store.sh +230 -0
  110. package/payload/mishkan/scripts/mishkan-ingest.sh +53 -3
  111. package/payload/mishkan/scripts/promote-curated.sh +77 -0
  112. package/payload/mishkan/scripts/reset-knowledge-data.sh +70 -0
  113. package/payload/mishkan/scripts/validate-research-log.sh +27 -0
  114. package/payload/mishkan/skills/baruch-research-reporting-craft/SKILL.md +42 -8
  115. package/payload/mishkan/skills/cognee-promote/SKILL.md +13 -0
  116. package/payload/mishkan/skills/cognee-quickstart/SKILL.md +7 -1
  117. package/payload/mishkan/skills/ezra-research-formulation-craft/SKILL.md +9 -0
  118. package/payload/mishkan/skills/mishkan-ingest/SKILL.md +22 -0
  119. package/payload/mishkan/skills/mishkan-init/SKILL.md +31 -19
  120. package/payload/mishkan/skills/shemaiah-evaluation-craft/SKILL.md +33 -5
  121. package/payload/mishkan/skills/team-lead-craft/SKILL.md +66 -0
  122. package/payload/mishkan/templates/mcp.json +7 -2
  123. package/payload/mishkan/templates/observability-log.schema.json +1 -1
  124. package/payload/mishkan/templates/research-log.schema.json +15 -1
  125. package/payload/mishkan/workflows/README.md +19 -13
  126. package/payload/mishkan/workflows/chosheb-feature-ship.js +63 -18
  127. package/payload/mishkan/workflows/migdal-dr-drill.js +4 -0
  128. package/payload/mishkan/workflows/migdal-infra-change.js +48 -7
  129. package/payload/mishkan/workflows/mishkan-architecture-panel.js +4 -0
  130. package/payload/mishkan/workflows/mishkan-blast-radius.js +4 -0
  131. package/payload/mishkan/workflows/mishkan-codebase-audit.js +4 -0
  132. package/payload/mishkan/workflows/mishkan-deep-research.js +4 -0
  133. package/payload/mishkan/workflows/mishkan-init.js +4 -0
  134. package/payload/mishkan/workflows/mishkan-knowledge-gap-discovery.js +4 -0
  135. package/payload/mishkan/workflows/mishkan-migration-wave.js +4 -0
  136. package/payload/mishkan/workflows/mishkan-release-readiness.js +4 -0
  137. package/payload/mishkan/workflows/mishkan-sprint-close.js +4 -0
  138. package/payload/mishkan/workflows/mishkan-standards-rollout.js +4 -0
  139. package/payload/mishkan/workflows/mishmar-security-gate.js +45 -6
  140. package/payload/mishkan/workflows/panim-ds-rollout.js +4 -0
  141. package/payload/mishkan/workflows/panim-feature-ship.js +132 -0
  142. package/payload/mishkan/workflows/sefer-release-notes.js +4 -0
  143. package/payload/mishkan/workflows/yasad-data-migration-wave.js +4 -0
  144. package/payload/mishkan/workflows/yasad-feature-ship.js +130 -0
  145. package/payload/mishkan/workflows/yasad-schema-evolution.js +4 -0
package/bin/mishkan.js CHANGED
@@ -382,9 +382,9 @@ DB_PASSWORD=${stackSecrets.DB_PASSWORD}
382
382
  DEFAULT_USER_EMAIL=${adminEmail}
383
383
  DEFAULT_USER_PASSWORD=${stackSecrets.DEFAULT_USER_PASSWORD}`;
384
384
 
385
- const final = `# Generated by mishkan-harness configure-knowledge on ${new Date().toISOString()}
385
+ const final = `# Generated by 'mishkan knowledge configure' on ${new Date().toISOString()}
386
386
  # Profile: ${profile.name}
387
- # Re-run \`npx mishkan-harness configure-knowledge\` to switch providers (secrets preserved).
387
+ # Re-run \`mishkan knowledge configure\` to switch providers (secrets preserved).
388
388
 
389
389
  ${profile.block(apiKeys)}
390
390
 
@@ -420,10 +420,11 @@ ${tail}`;
420
420
  console.log();
421
421
  console.log(c.bold("Quick reference (also written to ACCESS.txt):"));
422
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")}`);
423
+ console.log(` Memory · MCP ${c.cyan("http://127.0.0.1:7777/mcp")} ${c.dim("cognee-memory — shared session memory")}`);
424
+ console.log(` Memory · Graph UI ${c.cyan("http://127.0.0.1:7724")} ${c.dim(`${adminEmail} / DEFAULT_USER_PASSWORD`)}`);
425
+ console.log(` Memory · Neo4j ${c.cyan("http://127.0.0.1:7716")} ${c.dim("neo4j / GRAPH_DATABASE_PASSWORD")}`);
426
+ console.log(` Memory · REST ${c.cyan("http://127.0.0.1:7737")}`);
427
+ console.log(` Per-project work ${c.dim("provisioned per project at /mishkan-init (own port, embedded Ladybug) — ADR D-012")}`);
427
428
  console.log(` Curated · MCP ${c.cyan("http://127.0.0.1:7730/mcp")}`);
428
429
  console.log(` Curated · Graph ${c.cyan("http://127.0.0.1:7734")}`);
429
430
  console.log(` Curated · Neo4j ${c.cyan("http://127.0.0.1:7731")}`);
@@ -432,9 +433,8 @@ ${tail}`;
432
433
  console.log(` Ollama ${c.cyan("http://127.0.0.1:11434")} ${c.dim("local LLM / embeddings")}`);
433
434
  }
434
435
  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"));
436
+ console.log(c.bold("Bring up the knowledge stack:"));
437
+ console.log(c.dim(" mishkan knowledge-stack up") + c.dim(" (guided: preflights config, then memory :7777 + curated :7730)"));
438
438
  console.log(c.dim(`\n Full guide (incl. SSH-tunnel for remote hosts): ${tilde(accessPath)}`));
439
439
  }
440
440
 
@@ -464,16 +464,20 @@ endpoint needs, and how to reach them from a remote machine. Keep it private:
464
464
  it carries plaintext passwords. Mode 0600. Gitignored.
465
465
 
466
466
  ${sep}
467
- Cognee WORK store (per-project knowledge graph)
467
+ Cognee MEMORY store (shared session memory — alias cognee-memory, :7777)
468
468
  ${sep}
469
469
 
470
- MCP endpoint : http://127.0.0.1:7777/mcp
471
- - The HTTP transport agents call to query/add knowledge.
470
+ This is the kept Neo4j box, repurposed to hold only claude_code_memory (per-client
471
+ session memory). Per-project KNOWLEDGE lives in SEPARATE per-project work stores
472
+ (embedded Ladybug, own port each), provisioned by ensure-work-store.sh at
473
+ /mishkan-init — ADR D-012. This box is no longer the project work store.
474
+
475
+ MCP endpoint : http://127.0.0.1:7777/mcp (alias: cognee-memory)
472
476
  - Health check: \`curl -sf http://127.0.0.1:7777/mcp\` returns 406 = healthy
473
477
  (the endpoint requires the MCP handshake; a vanilla GET is rejected).
474
478
 
475
479
  Cognee Graph Explorer : http://127.0.0.1:7724
476
- - Web UI to browse the work graph.
480
+ - Web UI to browse the session-memory graph.
477
481
  - Login email : ${adminEmail}
478
482
  - Login password : ${secrets.DEFAULT_USER_PASSWORD}
479
483
 
@@ -481,7 +485,7 @@ Cognee Backend REST : http://127.0.0.1:7737
481
485
  - Backend API the Graph Explorer calls. Same creds as above.
482
486
 
483
487
  Neo4j Browser : http://127.0.0.1:7716
484
- - Direct cypher access to the work graph (read-only recommended).
488
+ - Direct cypher access to the session-memory graph (read-only recommended).
485
489
  - Username : ${secrets.GRAPH_DATABASE_USERNAME}
486
490
  - Password : ${secrets.GRAPH_DATABASE_PASSWORD}
487
491
  - Bolt URI : bolt://127.0.0.1:7709 (for desktop neo4j clients)
@@ -500,8 +504,8 @@ Web login : ${adminEmail} / ${secrets.DEFAULT_USER_PASSWORD}
500
504
  Neo4j login : ${secrets.GRAPH_DATABASE_USERNAME} / ${secrets.GRAPH_DATABASE_PASSWORD}
501
505
 
502
506
  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.
507
+ default (set by 'mishkan knowledge configure'). The compose stack reads .env.curated
508
+ for the curated containers — re-run 'mishkan knowledge configure' to sync both.
505
509
  ${ollamaSection}
506
510
  ${sep}
507
511
  Reaching a remote host (SSH tunnel)
@@ -539,7 +543,7 @@ ${sep}
539
543
  Re-running this wizard
540
544
  ${sep}
541
545
 
542
- npx mishkan-harness configure-knowledge
546
+ mishkan knowledge configure (or: npx mishkan-harness knowledge configure)
543
547
 
544
548
  Re-runs preserve the three local secrets (neo4j, pg, admin) so an
545
549
  initialised neo4j volume keeps working. Both .env and ACCESS.txt are
@@ -551,18 +555,17 @@ regenerated; previous .env is backed up to .env.bak.
551
555
 
552
556
  function installObservabilityStack() {
553
557
  console.log();
554
- console.log(c.bold(c.cyan("Observability stack")));
558
+ console.log(" " + c.bold(c.cyan("Observability")) + c.dim(" live cross-session daemon + TUI"));
555
559
  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+."));
560
+ " Aggregates the harness event bus into a live snapshot you can watch.\n" +
561
+ " Needs `uv` (https://astral.sh/uv) + Python 3.11+ · docs/design/MISHKAN_observability.md."));
559
562
 
560
563
  if (!commandExists("uv")) {
561
564
  console.log(c.yellow(" uv not found — skipping observability install."));
562
565
  console.log(c.dim(
563
566
  " Install uv with:\n" +
564
567
  " curl -LsSf https://astral.sh/uv/install.sh | sh\n" +
565
- " Then re-run: npx mishkan-harness observability"));
568
+ " Then re-run: npx mishkan-harness observability install"));
566
569
  return { installed: false, reason: "uv-missing" };
567
570
  }
568
571
 
@@ -585,15 +588,35 @@ function installObservabilityStack() {
585
588
  { stdio: "inherit" });
586
589
  if (r2.status !== 0) { warn("mishkan-watch install failed"); return { installed: false, reason: "install-failed" }; }
587
590
 
588
- console.log(c.green(" ✓ observability stack installed"));
591
+ console.log(" " + c.green("✓ observability installed") + c.dim(" · 1 executable: mishkan-watch"));
589
592
  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"));
593
+ " Open the TUI mishkan-watch (auto-starts the daemon)\n" +
594
+ " Two-terminal mishkan-watchd start, then mishkan-watch --no-autostart\n" +
595
+ " Stop the daemon mishkan-watchd stop\n" +
596
+ " Start at login mishkan-watchd install-service"));
594
597
  return { installed: true };
595
598
  }
596
599
 
600
+ // ─── post-install sign-off ───────────────────────────────────────────────────
601
+ // The wordmark + a short, builder-to-builder note. ASCII art (ANSI Shadow); the
602
+ // block glyphs are plain Unicode and render uncolored under NO_COLOR.
603
+ const WORDMARK = [
604
+ "███╗ ███╗██╗███████╗██╗ ██╗██╗ ██╗ █████╗ ███╗ ██╗",
605
+ "████╗ ████║██║██╔════╝██║ ██║██║ ██╔╝██╔══██╗████╗ ██║",
606
+ "██╔████╔██║██║███████╗███████║█████╔╝ ███████║██╔██╗ ██║",
607
+ "██║╚██╔╝██║██║╚════██║██╔══██║██╔═██╗ ██╔══██║██║╚██╗██║",
608
+ "██║ ╚═╝ ██║██║███████║██║ ██║██║ ██╗██║ ██║██║ ╚████║",
609
+ "╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
610
+ ];
611
+
612
+ function printBanner(version) {
613
+ console.log();
614
+ for (const line of WORDMARK) console.log(" " + c.cyan(line));
615
+ console.log();
616
+ console.log(" " + c.bold("מִשְׁכָּן") + c.dim(" · a dwelling place for your engineering work"));
617
+ console.log(" " + c.dim(`v${version} · 45 agents · 6 teams · research pipeline · knowledge graph`));
618
+ }
619
+
597
620
  async function install() {
598
621
  const totalPhases = 7;
599
622
  log(`installing into ${tilde(CLAUDE)} (home resolved at runtime)`);
@@ -606,6 +629,23 @@ async function install() {
606
629
  ensureDir(join(MISHKAN, "logs"));
607
630
  ensureDir(join(MISHKAN, "cognee"));
608
631
 
632
+ // D-017: place the model-routing overlay once (the engineer's tier overrides).
633
+ // copyDir ships no such file, so it is never clobbered on refresh — place-once,
634
+ // then preserve (same philosophy as engineer-standards.md). `mishkan model` edits it.
635
+ const overlayPath = join(MISHKAN, "config", "model-routing.local.yaml");
636
+ if (!existsSync(overlayPath)) {
637
+ ensureDir(join(MISHKAN, "config"));
638
+ writeFileSync(overlayPath,
639
+ "# MISHKAN model-routing OVERLAY (D-017) — your per-agent tier overrides.\n" +
640
+ "# Preserved across `mishkan install`. Managed by `mishkan model set/reset`\n" +
641
+ "# (hand-edits are fine). Entries here WIN over config/model-routing.yaml.\n" +
642
+ "# Empty = no overrides (shipped defaults apply). Tiers: opus, sonnet, haiku, fable.\n\n" +
643
+ "agents: {}\n");
644
+ log("placed model-routing overlay ~/.claude/mishkan/config/model-routing.local.yaml");
645
+ } else {
646
+ log("preserved your ~/.claude/mishkan/config/model-routing.local.yaml");
647
+ }
648
+
609
649
  // D-011 Phase 2: rebuild the universal skill-discovery index at install
610
650
  // time so the router has a live index.json before the first session boots.
611
651
  // The SessionStart hook keeps it fresh thereafter; this seeds it.
@@ -677,7 +717,7 @@ async function install() {
677
717
  if (await promptYN(" Install observability stack now?", true)) {
678
718
  installObservabilityStack();
679
719
  } else {
680
- console.log(c.dim(" Skipped. Re-run later: npx mishkan-harness observability"));
720
+ console.log(c.dim(" Skipped. Re-run later: npx mishkan-harness observability install"));
681
721
  }
682
722
 
683
723
  // Auto-symlink ~/.local/bin/mishkan -> this package's bin/mishkan.js so
@@ -717,39 +757,58 @@ async function install() {
717
757
  }
718
758
  const pathHasLocalBin = (process.env.PATH || "").split(":").includes(localBin);
719
759
 
760
+ printBanner(version);
761
+
762
+ // Whether the bare `mishkan` command works yet (symlinked AND on PATH) decides
763
+ // how we spell the commands below — bare, or via `npx mishkan-harness`.
764
+ const onPath = directAccess === "linked" && pathHasLocalBin;
765
+ const m = onPath ? "mishkan" : "npx mishkan-harness";
766
+
720
767
  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"));
768
+ console.log(" " + c.green("✓") + c.dim(" It's in place — every agent, rule, skill and command is"));
769
+ console.log(c.dim(" wired into ~/.claude and ready to work."));
770
+
771
+ console.log();
772
+ console.log(" " + c.bold(c.cyan("▸ Start here")));
773
+ console.log(c.dim(" Open a Claude session and talk to ") + "Nehemiah" + c.dim(" — your PM —"));
774
+ console.log(c.dim(" or run ") + "/mishkan-init" + c.dim(" inside a project to scaffold it."));
775
+
776
+ console.log();
777
+ console.log(" " + c.bold(c.cyan("▸ Commands")) + (onPath ? c.dim(" on your PATH now — no npx needed") : ""));
778
+ {
779
+ const rows = [
780
+ ["knowledge configure", "LLM provider + cognee secrets"],
781
+ ["knowledge-stack up", "bring the knowledge layer up (guided)"],
782
+ ["status", "install state + live stack health"],
783
+ ["project-work-store up", "this project's own store"],
784
+ ["knowledge ingest docs/…", "add docs to memory"],
785
+ ["observability open", "the live TUI"],
786
+ ];
787
+ const w = Math.max(...rows.map(([s]) => `${m} ${s}`.length)) + 3;
788
+ for (const [s, d] of rows) console.log(" " + `${m} ${s}`.padEnd(w) + c.dim(d));
752
789
  }
790
+ if (directAccess === "linked" && !pathHasLocalBin)
791
+ console.log(c.dim(" (add ~/.local/bin to PATH for the bare `mishkan` command)"));
792
+ else if (directAccess !== "linked")
793
+ console.log(c.dim(` (for a bare \`mishkan\`: ln -sf ${linkTarget} ~/.local/bin/mishkan)`));
794
+
795
+ // Knowledge stack (Cognee) is opt-in. The three stores answer three different
796
+ // questions (D-008/D-012) — name each one plainly here, then the two bring-up
797
+ // steps. The wizard + ACCESS.txt carry the full URL/cred detail.
798
+ console.log();
799
+ console.log(" " + c.bold(c.cyan("▸ Knowledge")) + c.dim(" three cognee stores + your code graph — optional, opt-in"));
800
+ console.log(" " + "memory".padEnd(9) + c.cyan(":7777") + c.dim(" what you learn across your sessions — kept and shared"));
801
+ console.log(" " + "curated".padEnd(9) + c.cyan(":7730") + c.dim(" a reference library you mostly read from"));
802
+ console.log(" " + "work".padEnd(9) + " " + c.dim("this project's private notes — never shared with other"));
803
+ console.log(c.dim(" projects; created the first time you run /mishkan-init"));
804
+ console.log(" " + "graphify".padEnd(9) + " " + c.dim("this project's code structure — a separate CLI, no server"));
805
+ console.log(c.dim(" (refresh with `mishkan code-graph scan`)"));
806
+ console.log();
807
+ console.log(c.dim(" Bring it up — two commands:"));
808
+ console.log(c.dim(" 1. ") + `${m} knowledge configure` + c.dim(" → writes .env + ACCESS.txt"));
809
+ console.log(c.dim(" 2. ") + `${m} knowledge-stack up` + c.dim(" → starts memory + curated (~5min first boot)"));
810
+ console.log(c.dim(" Aliases cognee-memory / cognee-curated · full URLs + creds in ACCESS.txt."));
811
+ console.log();
753
812
  }
754
813
 
755
814
  function uninstallObservabilityHint() {
@@ -820,7 +879,10 @@ function status() {
820
879
  log(`installed v${st.version} (${st.installedAt})`);
821
880
  log(`linked: agents=${count("agents")}, skills=${count("skills")}, commands=${count("commands")}, mishkan hooks=${hooks}`);
822
881
  log(`runtime profile: ${existsSync(join(MISHKAN,"profile.md")) ? "present" : "MISSING"}`);
823
- log(`cognee dir: ${existsSync(join(MISHKAN,"cognee")) ? "present (deploy the container to activate the graph)" : "missing"}`);
882
+ log(`cognee dir: ${existsSync(join(MISHKAN,"cognee")) ? "present" : "missing"}`);
883
+ // Fold in live knowledge-stack health so `mishkan status` answers "how's mishkan"
884
+ // (install + infra) in one view; `mishkan knowledge-stack status` is the detail.
885
+ if (existsSync(join(MISHKAN, "cognee")) && commandExists("docker")) printStackHealth();
824
886
  }
825
887
 
826
888
  // ─── org reference ─────────────────────────────────────────────────────────
@@ -919,16 +981,437 @@ function printOrgRef({ json = false } = {}) {
919
981
  console.log(c.dim(`source: ${data.generated_from || "docs/design/MISHKAN_agent_aliases.md"}`));
920
982
  }
921
983
 
984
+ // ─── backend control surface (ADR D-015) ────────────────────────────────────
985
+ // One SEMANTIC control surface for the cognee knowledge layer the engineer
986
+ // otherwise drives by hand across scattered compose files + scripts. Every
987
+ // command is `mishkan <object> <verb>`. The CLI EXECUTES because the HUMAN
988
+ // invokes it — rule 5 forbids *agents* from running stateful ops, not the
989
+ // engineer's own tool (agents never get this bin; the TUI only surfaces, never
990
+ // runs). Destructive ops gate on a confirm. Wraps the real scripts/compose — it
991
+ // does not replace them, so the scripts stay the single source of truth.
992
+ const COGNEE_DIR = join(MISHKAN, "cognee");
993
+ const SCRIPTS_DIR = join(MISHKAN, "scripts");
994
+ // knowledge-stack overlay order (base → hardening → selfhosted backends).
995
+ const STACK_FILES = ["docker-compose.yml", "docker-compose.hardening.yml", "docker-compose.selfhosted.yml"];
996
+ const stackComposeArgs = () => STACK_FILES.flatMap(f => ["-f", f]);
997
+
998
+ function requireCogneeDir() {
999
+ if (!existsSync(COGNEE_DIR)) {
1000
+ console.error(c.red("cognee dir not found: ") + tilde(COGNEE_DIR));
1001
+ console.log("run the installer first: " + c.bold("mishkan install"));
1002
+ process.exit(1);
1003
+ }
1004
+ }
1005
+
1006
+ // Derive a project slug from the current dir (matches the scripts' sanitisation).
1007
+ function slugFromCwd() {
1008
+ return (process.cwd().split("/").pop() || "")
1009
+ .toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1010
+ }
1011
+
1012
+ // Live health of every mishkan-* container. Used by `knowledge-stack status`
1013
+ // and folded into `mishkan status`.
1014
+ function printStackHealth() {
1015
+ const r = spawnSync("docker",
1016
+ ["ps", "-a", "--filter", "name=mishkan-", "--format", "{{.Names}}\t{{.Status}}"],
1017
+ { encoding: "utf8" });
1018
+ if (r.status !== 0) { console.log(" " + c.dim("(docker not available — knowledge-stack health unknown)")); return; }
1019
+ const lines = (r.stdout || "").trim();
1020
+ console.log("");
1021
+ console.log(" " + c.bold("knowledge-stack — containers"));
1022
+ console.log("");
1023
+ if (!lines) { console.log(" " + c.dim("none up — start it: ") + c.bold("mishkan knowledge-stack up")); console.log(""); return; }
1024
+ for (const line of lines.split("\n")) {
1025
+ const [name, ...rest] = line.split("\t");
1026
+ const status = rest.join(" ");
1027
+ const dot = /unhealthy/.test(status) ? c.yellow("⚠")
1028
+ : /Exited|Created|Dead|Restarting/.test(status) ? c.red("✗")
1029
+ : c.green("●");
1030
+ console.log(" " + dot + " " + (name || "").padEnd(28) + c.dim(status));
1031
+ }
1032
+ console.log("");
1033
+ }
1034
+
1035
+ // Preflight: what's missing before the knowledge-stack can come up. Each gap
1036
+ // carries a one-line fix, so `up` never dumps a cryptic docker error — it tells
1037
+ // you what to finish and stops, instead of failing halfway.
1038
+ function preflightStack() {
1039
+ const gaps = [];
1040
+ if (!commandExists("docker")) {
1041
+ gaps.push(["Docker isn't available on PATH", "install Docker and start the daemon, then re-run"]);
1042
+ }
1043
+ const env = join(COGNEE_DIR, ".env");
1044
+ if (!existsSync(env)) {
1045
+ gaps.push(["cognee .env not configured (LLM provider + local secrets)",
1046
+ "run " + c.bold("mishkan knowledge configure")]);
1047
+ } else {
1048
+ let txt = ""; try { txt = readFileSync(env, "utf8"); } catch {}
1049
+ if (!/^\s*COGNEE_MCP_REF=\S/m.test(txt)) {
1050
+ gaps.push(["COGNEE_MCP_REF (pinned cognee image tag) not set in cognee/.env",
1051
+ "run " + c.bold("mishkan knowledge configure")]);
1052
+ }
1053
+ }
1054
+ return gaps;
1055
+ }
1056
+
1057
+ function guideGaps(gaps, retryCmd) {
1058
+ console.log(c.yellow("\n Not ready yet — finish setup first:\n"));
1059
+ gaps.forEach(([problem, fix], i) => {
1060
+ console.log(" " + c.bold(`${i + 1}. ${problem}`));
1061
+ console.log(" " + c.dim("→ ") + fix + "\n");
1062
+ });
1063
+ console.log(" Then re-run " + c.bold(retryCmd) + ".\n");
1064
+ }
1065
+
1066
+ async function stackUp(opts) {
1067
+ requireCogneeDir();
1068
+ const gaps = preflightStack();
1069
+ if (gaps.length) { guideGaps(gaps, "mishkan knowledge-stack up"); process.exit(1); }
1070
+ const build = opts.has("--build");
1071
+ const args = ["compose", ...stackComposeArgs(), "up", "-d"];
1072
+ if (build) args.push("--build");
1073
+ log("starting the knowledge-stack" + (build ? " (rebuilding image — first build only)…" : "…"));
1074
+ const r = spawnSync("docker", args, { stdio: "inherit", cwd: COGNEE_DIR });
1075
+ if (r.status !== 0) { warn("knowledge-stack up failed"); process.exit(r.status || 1); }
1076
+ log("ensuring the curated reference box…");
1077
+ spawnSync("bash", [join(SCRIPTS_DIR, "ensure-curated-box.sh")], { stdio: "inherit", cwd: COGNEE_DIR });
1078
+ console.log(c.green("\n✓ knowledge-stack up.") + c.dim(" First boot is slow (~4-5 min) — check with ") + c.bold("mishkan status") + c.dim("."));
1079
+ console.log(" Then you can:");
1080
+ console.log(" " + c.bold("mishkan project-work-store up") + c.dim(" provision this project's private store"));
1081
+ console.log(" " + c.bold("mishkan knowledge ingest docs/…") + c.dim(" add documents to memory"));
1082
+ console.log(" " + c.bold("mishkan observability open") + c.dim(" watch it live\n"));
1083
+ }
1084
+
1085
+ async function stackDown({ confirm = true } = {}) {
1086
+ requireCogneeDir();
1087
+ if (confirm) {
1088
+ const ok = await promptYN("Stop the knowledge-stack? Containers stop; volumes/data survive.", false);
1089
+ if (!ok) { console.log("aborted."); return; }
1090
+ }
1091
+ spawnSync("docker", ["compose", ...stackComposeArgs(), "down"], { stdio: "inherit", cwd: COGNEE_DIR });
1092
+ // The curated box is its own compose project — bring it down too.
1093
+ if (existsSync(join(COGNEE_DIR, ".env.curated"))) {
1094
+ spawnSync("docker", ["compose", "--env-file", ".env.curated", "-f", "docker-compose.curated.yml", "down"],
1095
+ { stdio: "inherit", cwd: COGNEE_DIR });
1096
+ }
1097
+ console.log(c.green("✓ knowledge-stack stopped.") + c.dim(" (project work stores are separate — `mishkan project-work-store down`)"));
1098
+ }
1099
+
1100
+ async function knowledgeStackCmd(argv) {
1101
+ const sub = argv[0] || "status";
1102
+ const opts = new Set(argv.slice(1));
1103
+ if (sub === "status") { requireCogneeDir(); printStackHealth(); return; }
1104
+ if (sub === "up") { await stackUp(opts); return; }
1105
+ if (sub === "down") { await stackDown({ confirm: true }); return; }
1106
+ if (sub === "restart") { await stackDown({ confirm: false }); await stackUp(opts); return; }
1107
+ console.error("unknown subcommand: " + sub);
1108
+ console.log("use one of: up [--build] | down | restart | status");
1109
+ process.exit(1);
1110
+ }
1111
+
1112
+ function knowledgeIngest(argv) {
1113
+ const script = join(SCRIPTS_DIR, "mishkan-ingest.sh");
1114
+ if (!existsSync(script)) { console.error(c.red("mishkan-ingest.sh not found — run `mishkan install` first.")); process.exit(1); }
1115
+ // Run in the user's CWD: the script derives the project store from basename($PWD).
1116
+ const r = spawnSync("bash", [script, ...argv], { stdio: "inherit", cwd: process.cwd() });
1117
+ process.exit(r.status || 0);
1118
+ }
1119
+
1120
+ // D-016 — engineer-gated promotion of research-found resources into the shared
1121
+ // curated library. Baruch queues candidates (one JSON object per line) into
1122
+ // ~/.claude/mishkan/curated-candidates.jsonl; this walks them, asks per candidate,
1123
+ // and on approval runs the ADDITIVE promote-curated.sh (no prune, dedup by url).
1124
+ // Stateful by design — a human runs this CLI; agents never get the bin (rule 5).
1125
+ async function knowledgeCurate() {
1126
+ const queue = join(MISHKAN, "curated-candidates.jsonl");
1127
+ if (!existsSync(queue)) {
1128
+ console.log("No pending curated-library candidates.");
1129
+ console.log(c.dim(" Baruch queues them here when a resolved research run finds a reusable,"));
1130
+ console.log(c.dim(" not-yet-curated resource: " + tilde(queue)));
1131
+ return;
1132
+ }
1133
+ const lines = readFileSync(queue, "utf8").split("\n").map(s => s.trim()).filter(Boolean);
1134
+ if (lines.length === 0) { console.log("No pending curated-library candidates."); return; }
1135
+ const script = join(SCRIPTS_DIR, "promote-curated.sh");
1136
+ if (!existsSync(script)) { console.error(c.red("promote-curated.sh not found — run `mishkan install` first.")); process.exit(1); }
1137
+
1138
+ const tmp = join(COGNEE_DIR, ".curate-candidate.json");
1139
+ const remaining = []; // kept for retry (e.g. container down)
1140
+ let approved = 0, rejected = 0;
1141
+ const processedOut = [];
1142
+ for (const line of lines) {
1143
+ let cand;
1144
+ try { cand = JSON.parse(line); }
1145
+ catch { console.error(c.red("skipping malformed queue line: ") + line.slice(0, 80)); continue; }
1146
+ console.log();
1147
+ console.log(c.bold(cand.name || "(unnamed)") + c.dim(" [" + (cand.team || "?") + " · " + (cand.problem_class || "?") + "]"));
1148
+ console.log(" " + (cand.url || c.red("(no url)")));
1149
+ if (cand.why) console.log(c.dim(" why: " + cand.why));
1150
+ const ok = await promptYN("Promote this into the shared curated library?", false);
1151
+ if (!ok) { rejected++; processedOut.push(JSON.stringify({ ...cand, decision: "rejected" })); console.log(c.dim(" rejected — nothing written.")); continue; }
1152
+ writeFileSync(tmp, JSON.stringify(cand) + "\n");
1153
+ const r = spawnSync("bash", [script, tmp], { stdio: "inherit" });
1154
+ if (r.status === 0) { approved++; processedOut.push(JSON.stringify({ ...cand, decision: "approved" })); }
1155
+ else { console.error(c.red(" promotion failed (see above) — kept in the queue for retry.")); remaining.push(line); }
1156
+ }
1157
+ try { if (existsSync(tmp)) rmSync(tmp); } catch { /* best-effort cleanup */ }
1158
+ // Rewrite the queue with only the lines kept for retry; record decisions durably.
1159
+ writeFileSync(queue, remaining.length ? remaining.join("\n") + "\n" : "");
1160
+ if (processedOut.length) {
1161
+ const log = join(MISHKAN, "curated-candidates.processed.jsonl");
1162
+ const prev = existsSync(log) ? readFileSync(log, "utf8") : "";
1163
+ writeFileSync(log, prev + processedOut.join("\n") + "\n");
1164
+ }
1165
+ console.log();
1166
+ console.log(c.green(`curate: ${approved} approved, ${rejected} rejected`) + (remaining.length ? c.dim(`, ${remaining.length} kept for retry`) : ""));
1167
+ }
1168
+
1169
+ // D-017 cleanup — full reset of the knowledge layer to the stable baseline.
1170
+ // Wipes all work stores, prunes cognee-memory, re-seeds curated. Stateful
1171
+ // (docker rm / docker exec): the human runs it; the script type-to-confirms.
1172
+ function knowledgeReset(argv) {
1173
+ const script = join(SCRIPTS_DIR, "reset-knowledge-data.sh");
1174
+ if (!existsSync(script)) { console.error(c.red("reset-knowledge-data.sh not found — run `mishkan install` first.")); process.exit(1); }
1175
+ warn("FULL knowledge-data reset — wipes all work stores, prunes memory, re-seeds curated to baseline.");
1176
+ const r = spawnSync("bash", [script, ...argv], { stdio: "inherit" });
1177
+ process.exit(r.status || 0);
1178
+ }
1179
+
1180
+ async function knowledgeCmd(argv) {
1181
+ const sub = argv[0];
1182
+ if (sub === "configure") { await configureKnowledge(); return; }
1183
+ if (sub === "ingest") { knowledgeIngest(argv.slice(1)); return; }
1184
+ if (sub === "curate") { await knowledgeCurate(); return; }
1185
+ if (sub === "reset") { knowledgeReset(argv.slice(1)); return; }
1186
+ console.error("usage: mishkan knowledge <configure | ingest [paths…] | curate | reset>");
1187
+ console.log(" configure wizard: LLM provider + cognee secrets");
1188
+ console.log(" ingest [paths…] add docs to THIS project's store");
1189
+ console.log(" curate review + approve research-found resources into the shared curated library (D-016)");
1190
+ console.log(" reset wipe all stores → re-seed curated to the stable baseline (destructive; confirms)");
1191
+ process.exit(1);
1192
+ }
1193
+
1194
+ async function projectWorkStoreCmd(argv) {
1195
+ requireCogneeDir();
1196
+ const VERBS = new Set(["up", "down", "reset"]);
1197
+ let slug, sub;
1198
+ if (argv.length >= 2 && VERBS.has(argv[1])) { slug = argv[0]; sub = argv[1]; }
1199
+ else if (argv.length === 1 && VERBS.has(argv[0])) { slug = slugFromCwd(); sub = argv[0]; }
1200
+ else { console.error("usage: mishkan project-work-store [<slug>] <up|down|reset>"); process.exit(1); }
1201
+ if (!slug) { console.error("could not derive a project slug from the current directory; pass one explicitly."); process.exit(1); }
1202
+ const container = `mishkan-work-${slug}`;
1203
+ const volume = `${container}_work_data`;
1204
+ if (sub === "up") {
1205
+ const ps = spawnSync("docker", ["ps", "--format", "{{.Names}}"], { encoding: "utf8" });
1206
+ if (ps.status === 0 && !/mishkan-ollama|mishkan-cognee-mcp/.test(ps.stdout || "")) {
1207
+ warn("the knowledge-stack looks down — start it first: " + c.bold("mishkan knowledge-stack up"));
1208
+ }
1209
+ const r = spawnSync("bash", [join(SCRIPTS_DIR, "ensure-work-store.sh"), slug], { stdio: "inherit", cwd: COGNEE_DIR });
1210
+ if (r.status === 0) console.log(c.green(`✓ work store '${slug}' up.`) + c.dim(" add docs: ") + c.bold("mishkan knowledge ingest docs/…"));
1211
+ process.exit(r.status || 0);
1212
+ }
1213
+ if (sub === "down") {
1214
+ log(`removing ${container} (data volume ${volume} kept)…`);
1215
+ const r = spawnSync("docker", ["rm", "-f", container], { stdio: "inherit" });
1216
+ process.exit(r.status || 0);
1217
+ }
1218
+ if (sub === "reset") {
1219
+ const ok = await promptYN(`RESET ${slug}? Removes ${container} AND wipes its data volume ${volume}.`, false);
1220
+ if (!ok) { console.log("aborted."); return; }
1221
+ spawnSync("docker", ["rm", "-f", container], { stdio: "inherit" });
1222
+ const vr = spawnSync("docker", ["volume", "rm", volume], { stdio: "inherit" });
1223
+ if (vr.status !== 0) warn(`volume ${volume} not removed (already gone?)`);
1224
+ console.log(c.green(`✓ ${slug} reset.`) + c.dim(" re-provision: ") + c.bold(`mishkan project-work-store ${slug} up`));
1225
+ return;
1226
+ }
1227
+ // unreachable given the VERBS guard above — explicit so a future verb can't fall through silently
1228
+ console.error("internal error: unexpected project-work-store verb: " + sub);
1229
+ process.exit(1);
1230
+ }
1231
+
1232
+ function openWatchTui(argv) {
1233
+ if (!commandExists("mishkan-watch")) {
1234
+ console.error(c.red("mishkan-watch not on PATH."));
1235
+ console.log("install it: " + c.bold("mishkan observability install"));
1236
+ process.exit(1);
1237
+ }
1238
+ const r = spawnSync("mishkan-watch", argv, { stdio: "inherit" });
1239
+ process.exit(r.status || 0);
1240
+ }
1241
+
1242
+ function observabilityCmd(argv) {
1243
+ const sub = argv[0] || "install";
1244
+ if (sub === "install") { installObservabilityStack(); return; }
1245
+ if (sub === "open") { openWatchTui(argv.slice(1)); return; }
1246
+ console.error("usage: mishkan observability <install | open>");
1247
+ process.exit(1);
1248
+ }
1249
+
1250
+ function orgCmd(argv) {
1251
+ const sub = argv[0] || "show";
1252
+ if (sub === "show") { printOrgRef({ json: argv.includes("--json") }); return; }
1253
+ console.error("usage: mishkan org show [--json]");
1254
+ process.exit(1);
1255
+ }
1256
+
1257
+ // D-017 — user-editable model-tier routing. The hook (hooks/model-route.py) reads
1258
+ // the shipped default (config/model-routing.yaml) then overlays the engineer's
1259
+ // overrides (config/model-routing.local.yaml, preserved across installs). This
1260
+ // command edits ONLY the overlay — never the shipped default, never the 45
1261
+ // agent frontmatter files. So defaults keep flowing on update while your deltas persist.
1262
+ const ROUTING_DEFAULT = join(MISHKAN, "config", "model-routing.yaml");
1263
+ const ROUTING_LOCAL = join(MISHKAN, "config", "model-routing.local.yaml");
1264
+ const ORG_JSON = join(MISHKAN, "org", "org.json");
1265
+ const TIERS = new Set(["opus", "sonnet", "haiku", "fable"]);
1266
+ const DORMANT_TIERS = new Set(["fable"]); // valid but currently unavailable (Fable 5 suspended 2026-06-12)
1267
+
1268
+ // Minimal reader matching hooks/model-route.py parse_routing: agents map + default tier.
1269
+ function readRouting(path) {
1270
+ const agents = {};
1271
+ let dflt = "sonnet", section = null;
1272
+ if (!existsSync(path)) return { agents, dflt };
1273
+ for (const raw of readFileSync(path, "utf8").split("\n")) {
1274
+ const line = raw.split("#")[0].replace(/\s+$/, "");
1275
+ if (!line.trim()) continue;
1276
+ if (!/^[ \t]/.test(line)) { section = line.trim().replace(/:$/, ""); continue; }
1277
+ const idx = line.indexOf(":");
1278
+ if (idx < 0) continue;
1279
+ const key = line.slice(0, idx).trim();
1280
+ const val = line.slice(idx + 1).trim();
1281
+ if (!val) continue;
1282
+ if (section === "defaults" && key === "unlisted_agent" && TIERS.has(val)) dflt = val;
1283
+ else if (section === "agents" && TIERS.has(val)) agents[key] = val;
1284
+ }
1285
+ return { agents, dflt };
1286
+ }
1287
+
1288
+ function writeOverlay(map) {
1289
+ const keys = Object.keys(map).sort();
1290
+ let out =
1291
+ "# MISHKAN model-routing OVERLAY (D-017) — your per-agent tier overrides.\n" +
1292
+ "# Preserved across `mishkan install`. Managed by `mishkan model set/reset`\n" +
1293
+ "# (hand-edits are fine). Same shape as model-routing.yaml; entries here WIN\n" +
1294
+ "# over the shipped default. Empty = no overrides. Tiers: opus, sonnet, haiku, fable.\n\n";
1295
+ out += keys.length ? "agents:\n" + keys.map(k => ` ${k}: ${map[k]}`).join("\n") + "\n" : "agents: {}\n";
1296
+ ensureDir(dirname(ROUTING_LOCAL));
1297
+ writeFileSync(ROUTING_LOCAL, out);
1298
+ }
1299
+
1300
+ function allMishkanAgents() { return Object.keys(readRouting(ROUTING_DEFAULT).agents).sort(); }
1301
+
1302
+ function teamAgents(teamId) {
1303
+ if (!existsSync(ORG_JSON)) return null;
1304
+ let org; try { org = JSON.parse(readFileSync(ORG_JSON, "utf8")); } catch { return null; }
1305
+ const t = teamId.toLowerCase();
1306
+ for (const g of (org.groups || [])) {
1307
+ if ((g.id || "").toLowerCase() === t || (g.label || "").toLowerCase() === t) {
1308
+ return (g.agents || []).map(a => (typeof a === "string" ? a : a.alias)).filter(Boolean);
1309
+ }
1310
+ }
1311
+ return null;
1312
+ }
1313
+
1314
+ // Resolve <agent|team|all> to a list of known MISHKAN agent aliases.
1315
+ function resolveTargets(target, all) {
1316
+ if (target === "all") return all;
1317
+ if (all.includes(target)) return [target];
1318
+ const team = teamAgents(target);
1319
+ if (team && team.length) return team.filter(a => all.includes(a));
1320
+ return null;
1321
+ }
1322
+
1323
+ async function modelCmd(argv) {
1324
+ const sub = argv[0] || "show";
1325
+ const def = readRouting(ROUTING_DEFAULT);
1326
+ const overlay = readRouting(ROUTING_LOCAL).agents;
1327
+
1328
+ if (sub === "show") {
1329
+ const names = allMishkanAgents();
1330
+ if (names.length === 0) { console.error(c.red("no model-routing.yaml found — run `mishkan install` first.")); process.exit(1); }
1331
+ const nOver = Object.keys(overlay).length;
1332
+ console.log(c.bold("MISHKAN model-tier routing") + c.dim(" " + (nOver ? nOver + " override(s)" : "no overrides — shipped defaults")));
1333
+ let dormantSeen = false;
1334
+ for (const n of names) {
1335
+ const eff = overlay[n] || def.agents[n];
1336
+ const overridden = overlay[n] && overlay[n] !== def.agents[n];
1337
+ const dormant = DORMANT_TIERS.has(eff);
1338
+ if (dormant) dormantSeen = true;
1339
+ const mark = overridden ? c.bold(" ←override (was " + def.agents[n] + ")") : "";
1340
+ const warnTxt = dormant ? c.red(" ⚠ DORMANT — " + eff + " unavailable, will fail to spawn") : "";
1341
+ console.log(" " + n.padEnd(12) + " " + eff.padEnd(7) + mark + warnTxt);
1342
+ }
1343
+ console.log(c.dim("\n set: mishkan model set <agent|team|all> <tier> · revert: mishkan model reset [target]"));
1344
+ if (dormantSeen) console.log(c.red(" ⚠ agent(s) route to a dormant tier — re-tier them (mishkan model set …) or restore access."));
1345
+ return;
1346
+ }
1347
+
1348
+ if (sub === "set") {
1349
+ const target = argv[1], tier = argv[2];
1350
+ if (!target || !tier) { console.error("usage: mishkan model set <agent|team|all> <tier>"); process.exit(1); }
1351
+ if (!TIERS.has(tier)) { console.error(c.red(`invalid tier '${tier}' — valid: opus, sonnet, haiku, fable`)); process.exit(1); }
1352
+ const all = allMishkanAgents();
1353
+ const targets = resolveTargets(target, all);
1354
+ if (!targets) { console.error(c.red(`unknown agent or team '${target}'.`) + c.dim(" see: mishkan model show / mishkan org show")); process.exit(1); }
1355
+ if (DORMANT_TIERS.has(tier)) {
1356
+ warn(`'${tier}' is DORMANT — Claude Fable 5 was suspended 2026-06-12; agents routed here will fail to spawn.`);
1357
+ const ok = await promptYN(`Route ${targets.length} agent(s) to '${tier}' anyway?`, false);
1358
+ if (!ok) { console.log("aborted."); return; }
1359
+ }
1360
+ for (const a of targets) overlay[a] = tier;
1361
+ writeOverlay(overlay);
1362
+ console.log(c.green(`✓ set ${targets.length} agent(s) → ${tier}`) + c.dim(" overlay: " + tilde(ROUTING_LOCAL)));
1363
+ console.log(c.dim(" live on the next delegation (hook reads the overlay) · survives `mishkan install`."));
1364
+ return;
1365
+ }
1366
+
1367
+ if (sub === "reset") {
1368
+ const target = argv[1];
1369
+ if (!target) {
1370
+ const n = Object.keys(overlay).length;
1371
+ if (n === 0) { console.log("no overrides to reset."); return; }
1372
+ const ok = await promptYN(`Clear ALL ${n} routing override(s) — revert the whole fleet to shipped defaults?`, false);
1373
+ if (!ok) { console.log("aborted."); return; }
1374
+ writeOverlay({});
1375
+ console.log(c.green(`✓ cleared ${n} override(s) — fleet back to shipped defaults.`));
1376
+ return;
1377
+ }
1378
+ const all = allMishkanAgents();
1379
+ const targets = target === "all" ? Object.keys(overlay) : (resolveTargets(target, all) || []);
1380
+ let removed = 0;
1381
+ for (const a of targets) if (overlay[a]) { delete overlay[a]; removed++; }
1382
+ writeOverlay(overlay);
1383
+ console.log(removed ? c.green(`✓ reset ${removed} override(s) → shipped default.`) : `no overrides on '${target}'.`);
1384
+ return;
1385
+ }
1386
+
1387
+ console.error("usage: mishkan model <show | set <agent|team|all> <tier> | reset [agent|team|all]>");
1388
+ console.log(" show effective tier per agent (shipped default + your overrides)");
1389
+ console.log(" set <target> <tier> override a tier — target is an agent alias, a team id, or 'all'");
1390
+ console.log(" reset [target] drop override(s); no target = clear them all");
1391
+ console.log(" tiers: opus · sonnet · haiku · fable" + c.dim(" (fable dormant — suspended 2026-06-12)"));
1392
+ process.exit(1);
1393
+ }
1394
+
922
1395
  const cmd = process.argv[2];
923
1396
  const flags = new Set(process.argv.slice(3));
924
1397
  switch (cmd) {
1398
+ // self-management — object is the harness itself (bare verbs; `npx mishkan-harness <verb>`)
925
1399
  case "install": await install(); break;
926
1400
  case "uninstall": uninstall({ purge: flags.has("--purge") }); break;
927
1401
  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;
1402
+ // subsystems mishkan <object> <verb>
1403
+ case "knowledge": await knowledgeCmd(process.argv.slice(3)); break;
1404
+ case "knowledge-stack": await knowledgeStackCmd(process.argv.slice(3)); break;
1405
+ case "project-work-store": await projectWorkStoreCmd(process.argv.slice(3)); break;
931
1406
  case "code-graph": codeGraphCmd(process.argv.slice(3)); break;
1407
+ case "observability": observabilityCmd(process.argv.slice(3)); break;
1408
+ case "org": orgCmd(process.argv.slice(3)); break;
1409
+ case "model": await modelCmd(process.argv.slice(3)); break;
1410
+ // deprecated flat aliases — kept working (not advertised) so nothing breaks mid-migration
1411
+ case "configure-knowledge": await knowledgeCmd(["configure"]); break;
1412
+ case "ingest": knowledgeIngest(process.argv.slice(3)); break;
1413
+ case "watch": openWatchTui(process.argv.slice(3)); break;
1414
+ case "org-ref": orgCmd(["show", ...(flags.has("--json") ? ["--json"] : [])]); break;
932
1415
  case "help": case "--help": case "-h":
933
1416
  printHelp(); break;
934
1417
  default:
@@ -964,24 +1447,24 @@ function printHelp() {
964
1447
  console.log(c.dim(" After `install` the harness can symlink for direct access automatically."));
965
1448
  }
966
1449
  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)");
1450
+ console.log(c.bold("Manage the harness"));
1451
+ console.log(" " + c.bold(`${prefix} install`) + " Install / refresh into ~/.claude (idempotent)");
1452
+ console.log(" " + c.bold(`${prefix} uninstall [--purge]`) + " Remove it (--purge also drops y4nn-standards.md)");
1453
+ console.log(" " + c.bold(`${prefix} status`) + " Install state + live knowledge-stack health");
975
1454
  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");
1455
+ console.log(c.bold("Knowledge") + c.dim(" (you run these — agents never do)"));
1456
+ console.log(" " + c.bold(`${prefix} knowledge configure`) + " Wizard: LLM provider + cognee secrets");
1457
+ console.log(" " + c.bold(`${prefix} knowledge ingest [paths…]`) + " Add docs to THIS project's store");
1458
+ console.log(" " + c.bold(`${prefix} knowledge-stack up|down|restart|status`) + " The shared running infra (up = guided)");
1459
+ console.log(" " + c.bold(`${prefix} knowledge reset`) + " Wipe all stores → re-seed curated baseline (destructive)");
1460
+ console.log(" " + c.bold(`${prefix} project-work-store [<slug>] up|down|reset`) + " A project's own store");
980
1461
  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");
1462
+ console.log(c.bold("Inspect / observe"));
1463
+ console.log(" " + c.bold(`${prefix} code-graph status|open|scan`) + " The project's code graph (Graphify)");
1464
+ console.log(" " + c.bold(`${prefix} observability install|open`) + " The live monitor (daemon + TUI)");
1465
+ console.log(" " + c.bold(`${prefix} org show [--json]`) + " The 45-agent reference");
1466
+ console.log(" " + c.bold(`${prefix} model show|set|reset`) + " Re-tier agents (per-agent/team/all) — survives updates");
1467
+ console.log(" " + c.dim("(the TUI binary is ") + c.bold("mishkan-watch") + c.dim("; `mishkan-watchd start|stop|status` for manual daemon control)"));
985
1468
  console.log("");
986
1469
  console.log(c.bold("Inside a Claude Code session"));
987
1470
  console.log(" " + c.dim("Talk to Nehemiah (PM) in plain language — exploration mode is the default."));