mishkan-harness 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -172
- package/bin/mishkan.js +677 -16
- package/docs/design/MISHKAN_decisions.md +450 -8
- package/docs/design/MISHKAN_harness_design.md +1 -33
- package/docs/design/MISHKAN_observability.md +13 -9
- package/docs/research/graphify-token-saving-poc.md +108 -0
- package/docs/usage/09-workflows.md +74 -4
- package/docs/usage/10-observability.md +37 -13
- package/docs/usage/11-graphify.md +264 -0
- package/docs/usage/12-skill-discovery.md +294 -0
- package/docs/usage/README.md +5 -3
- package/package.json +1 -1
- package/payload/install/settings.hooks.json +24 -0
- package/payload/mishkan/AGENT_SPEC.md +25 -7
- package/payload/mishkan/agents/ahikam.md +4 -2
- package/payload/mishkan/agents/aholiab.md +4 -2
- package/payload/mishkan/agents/asaph.md +5 -2
- package/payload/mishkan/agents/baruch.md +19 -2
- package/payload/mishkan/agents/benaiah.md +4 -2
- package/payload/mishkan/agents/bezalel.md +4 -2
- package/payload/mishkan/agents/caleb.md +4 -2
- package/payload/mishkan/agents/deborah.md +4 -2
- package/payload/mishkan/agents/elasah.md +4 -2
- package/payload/mishkan/agents/eliashib.md +4 -2
- package/payload/mishkan/agents/ezra.md +4 -2
- package/payload/mishkan/agents/hanun.md +5 -2
- package/payload/mishkan/agents/hiram.md +5 -2
- package/payload/mishkan/agents/hizkiah.md +5 -2
- package/payload/mishkan/agents/huldah.md +4 -2
- package/payload/mishkan/agents/huram.md +4 -2
- package/payload/mishkan/agents/hushai.md +5 -2
- package/payload/mishkan/agents/igal.md +4 -2
- package/payload/mishkan/agents/ira.md +5 -2
- package/payload/mishkan/agents/jahaziel.md +5 -2
- package/payload/mishkan/agents/jakin.md +4 -2
- package/payload/mishkan/agents/jehonathan.md +5 -2
- package/payload/mishkan/agents/jehoshaphat.md +4 -2
- package/payload/mishkan/agents/joab.md +5 -2
- package/payload/mishkan/agents/joah.md +5 -2
- package/payload/mishkan/agents/maaseiah.md +4 -2
- package/payload/mishkan/agents/meremoth.md +5 -2
- package/payload/mishkan/agents/meshullam.md +5 -2
- package/payload/mishkan/agents/nathan.md +5 -2
- package/payload/mishkan/agents/nehemiah.md +4 -2
- package/payload/mishkan/agents/obed.md +4 -2
- package/payload/mishkan/agents/oholiab.md +5 -2
- package/payload/mishkan/agents/palal.md +5 -2
- package/payload/mishkan/agents/phinehas.md +4 -2
- package/payload/mishkan/agents/rehum.md +4 -2
- package/payload/mishkan/agents/salma.md +5 -2
- package/payload/mishkan/agents/seraiah.md +4 -2
- package/payload/mishkan/agents/shallum.md +5 -2
- package/payload/mishkan/agents/shaphan.md +4 -2
- package/payload/mishkan/agents/shemaiah.md +4 -2
- package/payload/mishkan/agents/shevna.md +5 -2
- package/payload/mishkan/agents/uriah.md +5 -2
- package/payload/mishkan/agents/zaccur.md +4 -2
- package/payload/mishkan/agents/zadok.md +5 -2
- package/payload/mishkan/agents/zerubbabel.md +4 -2
- package/payload/mishkan/commands/code-graph.md +35 -0
- package/payload/mishkan/commands/eval-baruch.md +57 -0
- package/payload/mishkan/commands/mishkan-org-reference.md +58 -0
- package/payload/mishkan/commands/mishkan-skills-misses.md +47 -0
- package/payload/mishkan/commands/mishkan-skills-reindex.md +33 -0
- package/payload/mishkan/commands/skills.md +35 -0
- package/payload/mishkan/commands/sprint-close.md +3 -1
- package/payload/mishkan/hooks/pre-tool-knowledge-route.sh +231 -0
- package/payload/mishkan/hooks/pre-tool-task-skill-route.sh +122 -0
- package/payload/mishkan/hooks/session-start-skill-index.sh +35 -0
- package/payload/mishkan/observability/schema.json +1 -1
- package/payload/mishkan/observability/watch/src/mishkan_watch/__main__.py +84 -2
- package/payload/mishkan/observability/watch/src/mishkan_watch/app.py +64 -3
- package/payload/mishkan/observability/watch/src/mishkan_watch/org_data.py +79 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/skills_data.py +267 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/agents.py +33 -1
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/knowledge.py +118 -9
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/live.py +102 -2
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/org.py +139 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/skills.py +187 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/usage.py +377 -0
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/workflows.py +199 -27
- package/payload/mishkan/observability/watch/src/mishkan_watch/theme.tcss +78 -4
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/__main__.py +36 -6
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/bus_tail.py +16 -3
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/graphify_tail.py +220 -0
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/worktree_poll.py +13 -2
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/state.py +157 -3
- package/payload/mishkan/observability/watchd/tests/test_state.py +6 -2
- package/payload/mishkan/org/org.json +141 -0
- package/payload/mishkan/scripts/pre-commit-eval-gate.sh +50 -0
- package/payload/mishkan/scripts/skill-discovery-indexer.py +462 -0
- package/payload/mishkan/scripts/skill-discovery-misses.py +173 -0
- package/payload/mishkan/scripts/skill-discovery-router.py +457 -0
- package/payload/mishkan/scripts/skill-discovery-smoke.sh +156 -0
- package/payload/mishkan/skills/graphify-query-craft/SKILL.md +162 -0
- package/payload/mishkan/skills/mishkan-init/SKILL.md +15 -0
- package/payload/mishkan/skills/skill-discovery/SKILL.md +120 -0
- package/payload/mishkan/templates/case-node.schema.json +1 -0
- package/payload/mishkan/templates/observability-log.schema.json +1 -0
- package/payload/mishkan/workflows/README.md +78 -13
- package/payload/mishkan/workflows/chosheb-feature-ship.js +75 -0
- package/payload/mishkan/workflows/migdal-dr-drill.js +94 -0
- package/payload/mishkan/workflows/migdal-infra-change.js +67 -0
- package/payload/mishkan/workflows/mishkan-blast-radius.js +144 -0
- package/payload/mishkan/workflows/mishkan-init.js +58 -0
- package/payload/mishkan/workflows/mishkan-knowledge-gap-discovery.js +121 -0
- package/payload/mishkan/workflows/mishkan-standards-rollout.js +153 -0
- package/payload/mishkan/workflows/mishmar-security-gate.js +83 -0
- package/payload/mishkan/workflows/panim-ds-rollout.js +83 -0
- package/payload/mishkan/workflows/sefer-release-notes.js +85 -0
- package/payload/mishkan/workflows/yasad-data-migration-wave.js +78 -0
- package/payload/mishkan/workflows/yasad-schema-evolution.js +76 -0
- package/payload/mishkan/templates/user-CLAUDE.md +0 -62
|
@@ -21,13 +21,14 @@ class KnowledgeTab(Container):
|
|
|
21
21
|
|
|
22
22
|
def __init__(self, **kwargs) -> None:
|
|
23
23
|
super().__init__(**kwargs)
|
|
24
|
-
self._state: dict[str, Any] = {"mcp_servers": {}, "cognee": {}}
|
|
24
|
+
self._state: dict[str, Any] = {"mcp_servers": {}, "cognee": {}, "graphify": {}}
|
|
25
25
|
self._recent_ops: deque[dict[str, Any]] = deque(maxlen=30)
|
|
26
26
|
|
|
27
27
|
def compose(self) -> ComposeResult:
|
|
28
28
|
with Horizontal(id="knowledge-stores"):
|
|
29
29
|
yield Static("", id="knowledge-work", classes="cognee-card")
|
|
30
30
|
yield Static("", id="knowledge-curated", classes="cognee-card")
|
|
31
|
+
yield Static("", id="knowledge-graphify", classes="cognee-card")
|
|
31
32
|
with Container(id="knowledge-ops"):
|
|
32
33
|
yield Static("RECENT OPS", classes="panel-title")
|
|
33
34
|
yield DataTable(id="knowledge-ops-table", cursor_type="row",
|
|
@@ -40,7 +41,7 @@ class KnowledgeTab(Container):
|
|
|
40
41
|
def on_mount(self) -> None:
|
|
41
42
|
try:
|
|
42
43
|
ops = self.query_one("#knowledge-ops-table", DataTable)
|
|
43
|
-
ops.add_columns("time", "store", "op", "
|
|
44
|
+
ops.add_columns("time", "store", "op", "detail", "metric")
|
|
44
45
|
mcp = self.query_one("#knowledge-mcp-table", DataTable)
|
|
45
46
|
mcp.add_columns("server", "status", "since", "last event")
|
|
46
47
|
except Exception:
|
|
@@ -64,11 +65,65 @@ class KnowledgeTab(Container):
|
|
|
64
65
|
if etype in ("cognee_op", "graphify_query", "graphify_scan"):
|
|
65
66
|
self._recent_ops.append(ev)
|
|
66
67
|
self._render_ops()
|
|
68
|
+
if etype == "cognee_op":
|
|
69
|
+
self._mutate_cognee(ev)
|
|
70
|
+
self._render_cards()
|
|
71
|
+
if etype in ("graphify_scan", "graphify_query"):
|
|
72
|
+
self._mutate_graphify(ev)
|
|
73
|
+
self._render_cards()
|
|
67
74
|
if etype == "mcp_server":
|
|
68
75
|
self._mutate_mcp(ev)
|
|
69
76
|
self._render_mcp()
|
|
70
77
|
self._render_cards()
|
|
71
78
|
|
|
79
|
+
def _mutate_graphify(self, ev: dict[str, Any]) -> None:
|
|
80
|
+
"""Update self._state['graphify'] from graphify_scan / graphify_query.
|
|
81
|
+
|
|
82
|
+
Aggregates: latest scan (nodes/edges/project/ts) + query counter.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
p = ev.get("payload") or {}
|
|
86
|
+
etype = ev.get("type")
|
|
87
|
+
g = self._state.setdefault("graphify", {})
|
|
88
|
+
if etype == "graphify_scan":
|
|
89
|
+
g["last_scan_project"] = p.get("project")
|
|
90
|
+
g["last_scan_at"] = p.get("scanned_at") or ev.get("ts")
|
|
91
|
+
g["nodes"] = p.get("nodes")
|
|
92
|
+
g["edges"] = p.get("edges")
|
|
93
|
+
g["communities"] = p.get("communities")
|
|
94
|
+
g["scans"] = (g.get("scans") or 0) + 1
|
|
95
|
+
elif etype == "graphify_query":
|
|
96
|
+
g["last_query_project"] = p.get("project")
|
|
97
|
+
g["last_query_at"] = ev.get("ts")
|
|
98
|
+
g["last_query_text"] = p.get("question") or p.get("file")
|
|
99
|
+
g["queries"] = (g.get("queries") or 0) + 1
|
|
100
|
+
except Exception:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
def _mutate_cognee(self, ev: dict[str, Any]) -> None:
|
|
104
|
+
"""Update self._state['cognee'][store] from a cognee_op probe event.
|
|
105
|
+
|
|
106
|
+
Without this, _render_cards keeps reading an empty dict and the
|
|
107
|
+
top work/curated cards stay at zero even though the daemon is
|
|
108
|
+
emitting healthy probe events every 30s.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
p = ev.get("payload") or {}
|
|
112
|
+
store = p.get("store")
|
|
113
|
+
if not store:
|
|
114
|
+
return
|
|
115
|
+
cognee = self._state.setdefault("cognee", {})
|
|
116
|
+
entry = cognee.setdefault(store, {})
|
|
117
|
+
entry["up"] = bool(p.get("up"))
|
|
118
|
+
entry["url"] = p.get("url") or entry.get("url", "")
|
|
119
|
+
if "nodes" in p:
|
|
120
|
+
entry["nodes"] = p.get("nodes")
|
|
121
|
+
entry["last_event"] = ev.get("ts")
|
|
122
|
+
if p.get("status_changed"):
|
|
123
|
+
entry["since"] = ev.get("ts")
|
|
124
|
+
except Exception:
|
|
125
|
+
return
|
|
126
|
+
|
|
72
127
|
def _mutate_mcp(self, ev: dict[str, Any]) -> None:
|
|
73
128
|
try:
|
|
74
129
|
p = ev.get("payload") or {}
|
|
@@ -97,6 +152,7 @@ class KnowledgeTab(Container):
|
|
|
97
152
|
try:
|
|
98
153
|
work_panel = self.query_one("#knowledge-work", Static)
|
|
99
154
|
cur_panel = self.query_one("#knowledge-curated", Static)
|
|
155
|
+
graphify_panel = self.query_one("#knowledge-graphify", Static)
|
|
100
156
|
except Exception:
|
|
101
157
|
return
|
|
102
158
|
mcps = self._state.get("mcp_servers") or {}
|
|
@@ -106,6 +162,40 @@ class KnowledgeTab(Container):
|
|
|
106
162
|
cur_entry = cognee_state.get("curated") or self._mcp_match(mcps, "cognee", ("curated", "7730"))
|
|
107
163
|
work_panel.update(self._card_for("work :7777", work_entry))
|
|
108
164
|
cur_panel.update(self._card_for("curated :7730", cur_entry))
|
|
165
|
+
graphify_panel.update(self._graphify_card(self._state.get("graphify") or {}))
|
|
166
|
+
|
|
167
|
+
def _graphify_card(self, g: dict[str, Any]) -> Text:
|
|
168
|
+
text = Text()
|
|
169
|
+
has_data = bool(g.get("last_scan_at") or g.get("last_query_at"))
|
|
170
|
+
dot = "●" if has_data else "○"
|
|
171
|
+
color = "#00D4AA" if has_data else "#F6AD55"
|
|
172
|
+
text.append(f"{dot} ", style=color)
|
|
173
|
+
text.append("graphify", style="bold")
|
|
174
|
+
text.append(" (code-graph)\n", style="dim")
|
|
175
|
+
text.append("\n")
|
|
176
|
+
nodes = g.get("nodes")
|
|
177
|
+
edges = g.get("edges")
|
|
178
|
+
if nodes is not None:
|
|
179
|
+
text.append(f"{nodes:>6}", style="bold")
|
|
180
|
+
text.append(" nodes ", style="dim")
|
|
181
|
+
if edges is not None:
|
|
182
|
+
text.append(f"{edges:,}", style="bold")
|
|
183
|
+
text.append(" edges", style="dim")
|
|
184
|
+
text.append("\n")
|
|
185
|
+
scans = g.get("scans") or 0
|
|
186
|
+
queries = g.get("queries") or 0
|
|
187
|
+
text.append(f"{scans:>6}", style="bold")
|
|
188
|
+
text.append(" scans ", style="dim")
|
|
189
|
+
text.append(f"{queries}", style="bold")
|
|
190
|
+
text.append(" queries\n", style="dim")
|
|
191
|
+
last_scan = g.get("last_scan_at")
|
|
192
|
+
if last_scan and len(str(last_scan)) > 19:
|
|
193
|
+
text.append("scan ", style="dim")
|
|
194
|
+
text.append(str(last_scan)[11:19], style="white")
|
|
195
|
+
text.append("\n")
|
|
196
|
+
if not has_data:
|
|
197
|
+
text.append("(no scan emitted yet)\n", style="dim italic")
|
|
198
|
+
return text
|
|
109
199
|
|
|
110
200
|
def _mcp_match(self, mcps: dict[str, Any], must_have: str,
|
|
111
201
|
prefer_tokens: tuple[str, ...]) -> dict[str, Any] | None:
|
|
@@ -151,18 +241,37 @@ class KnowledgeTab(Container):
|
|
|
151
241
|
table.clear()
|
|
152
242
|
for ev in list(self._recent_ops)[-20:]:
|
|
153
243
|
ts = (ev.get("ts") or "")[11:19]
|
|
244
|
+
etype = ev.get("type") or ""
|
|
154
245
|
p = ev.get("payload") or {}
|
|
155
|
-
store = p.get("store") or ("graphify" if
|
|
156
|
-
op = p.get("op") or
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
246
|
+
store = p.get("store") or ("graphify" if etype.startswith("graphify") else "?")
|
|
247
|
+
op = p.get("op") or etype.replace("graphify_", "g:")
|
|
248
|
+
# Each event family fills the "query/path" + "ms" columns
|
|
249
|
+
# with the most useful field it carries:
|
|
250
|
+
# cognee_op → up/down status, nodes count
|
|
251
|
+
# graphify_scan → project basename, nodes/edges
|
|
252
|
+
# graphify_query → question excerpt or filename
|
|
253
|
+
if etype == "cognee_op":
|
|
254
|
+
detail = ("up " if p.get("up") else "down ") + str(p.get("url") or "")
|
|
255
|
+
n = p.get("nodes")
|
|
256
|
+
metric = f"{n} nodes" if n is not None else "-"
|
|
257
|
+
elif etype == "graphify_scan":
|
|
258
|
+
detail = (p.get("project") or "").rsplit("/", 1)[-1] or "(?)"
|
|
259
|
+
nodes = p.get("nodes")
|
|
260
|
+
edges = p.get("edges")
|
|
261
|
+
metric = f"{nodes}n {edges}e" if nodes is not None and edges is not None else "-"
|
|
262
|
+
elif etype == "graphify_query":
|
|
263
|
+
detail = (p.get("question") or p.get("file") or "")[:40]
|
|
264
|
+
metric = p.get("query_type") or "query"
|
|
265
|
+
else:
|
|
266
|
+
detail = (p.get("query") or p.get("path") or "")[:40]
|
|
267
|
+
metric = f"{ev.get('duration_ms') or 0}ms"
|
|
268
|
+
style = "#F687B3" if "cognee" in etype else "#B794F4"
|
|
160
269
|
table.add_row(
|
|
161
270
|
Text(ts, style="dim"),
|
|
162
271
|
Text(str(store)[:10], style=style),
|
|
163
272
|
Text(str(op)[:10]),
|
|
164
|
-
Text(
|
|
165
|
-
Text(
|
|
273
|
+
Text(detail[:50]),
|
|
274
|
+
Text(metric, style="dim"),
|
|
166
275
|
)
|
|
167
276
|
|
|
168
277
|
def _render_mcp(self) -> None:
|
|
@@ -16,6 +16,27 @@ from textual.containers import Container, Horizontal, Vertical
|
|
|
16
16
|
from textual.widgets import RichLog, Static
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
|
|
19
|
+
# Module-level cache for the alias→role lookup (org.json is static).
|
|
20
|
+
_ROLE_CACHE: dict[str, str | None] | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _role_for(alias: str) -> str | None:
|
|
24
|
+
global _ROLE_CACHE
|
|
25
|
+
if _ROLE_CACHE is None:
|
|
26
|
+
try:
|
|
27
|
+
from ..org_data import load_org
|
|
28
|
+
org = load_org()
|
|
29
|
+
cache: dict[str, str | None] = {}
|
|
30
|
+
for grp in org.get("groups", []):
|
|
31
|
+
for ag in grp.get("agents", []):
|
|
32
|
+
a = (ag.get("alias") or "").lower()
|
|
33
|
+
if a:
|
|
34
|
+
cache[a] = ag.get("short") or ag.get("role")
|
|
35
|
+
_ROLE_CACHE = cache
|
|
36
|
+
except Exception:
|
|
37
|
+
_ROLE_CACHE = {}
|
|
38
|
+
return _ROLE_CACHE.get((alias or "").lower())
|
|
39
|
+
|
|
19
40
|
|
|
20
41
|
MAX_FEED_LINES = 200
|
|
21
42
|
|
|
@@ -116,6 +137,9 @@ class LiveTab(Container):
|
|
|
116
137
|
def __init__(self, **kwargs) -> None:
|
|
117
138
|
super().__init__(**kwargs)
|
|
118
139
|
self._state: dict[str, Any] = {}
|
|
140
|
+
# Project filter — None means show all; a Path means filter to that project.
|
|
141
|
+
# Toggled by the app via set_project_filter() in response to the 'p' key.
|
|
142
|
+
self._project_filter: Any = None
|
|
119
143
|
self._feed_buf: deque[Text] = deque(maxlen=MAX_FEED_LINES)
|
|
120
144
|
|
|
121
145
|
def compose(self) -> ComposeResult:
|
|
@@ -159,7 +183,7 @@ class LiveTab(Container):
|
|
|
159
183
|
self._render_active(self._state)
|
|
160
184
|
if etype == "worktree_change":
|
|
161
185
|
self._render_worktrees(self._state)
|
|
162
|
-
if etype in ("mcp_server", "cognee_op"):
|
|
186
|
+
if etype in ("mcp_server", "cognee_op", "graphify_scan", "graphify_query"):
|
|
163
187
|
self._render_knowledge(self._state)
|
|
164
188
|
|
|
165
189
|
def _mutate_state(self, ev: dict[str, Any]) -> None:
|
|
@@ -219,6 +243,29 @@ class LiveTab(Container):
|
|
|
219
243
|
existing["status"] = p.get("status") or existing.get("status", "?")
|
|
220
244
|
existing["last_event"] = ev.get("ts")
|
|
221
245
|
mcps[name] = existing
|
|
246
|
+
elif etype == "cognee_op":
|
|
247
|
+
p = ev.get("payload") or {}
|
|
248
|
+
store = p.get("store")
|
|
249
|
+
if store:
|
|
250
|
+
cognee = self._state.setdefault("cognee", {})
|
|
251
|
+
entry = cognee.setdefault(store, {})
|
|
252
|
+
entry["up"] = bool(p.get("up"))
|
|
253
|
+
entry["url"] = p.get("url") or entry.get("url", "")
|
|
254
|
+
if "nodes" in p:
|
|
255
|
+
entry["nodes"] = p.get("nodes")
|
|
256
|
+
entry["last_event"] = ev.get("ts")
|
|
257
|
+
elif etype in ("graphify_scan", "graphify_query"):
|
|
258
|
+
p = ev.get("payload") or {}
|
|
259
|
+
g = self._state.setdefault("graphify", {})
|
|
260
|
+
if etype == "graphify_scan":
|
|
261
|
+
g["nodes"] = p.get("nodes")
|
|
262
|
+
g["edges"] = p.get("edges")
|
|
263
|
+
g["communities"] = p.get("communities")
|
|
264
|
+
g["scans"] = (g.get("scans") or 0) + 1
|
|
265
|
+
g["last_scan_at"] = p.get("scanned_at") or ev.get("ts")
|
|
266
|
+
else:
|
|
267
|
+
g["queries"] = (g.get("queries") or 0) + 1
|
|
268
|
+
g["last_query_at"] = ev.get("ts")
|
|
222
269
|
elif etype == "session_stop" and sid:
|
|
223
270
|
sessions.pop(sid, None)
|
|
224
271
|
except Exception:
|
|
@@ -226,6 +273,38 @@ class LiveTab(Container):
|
|
|
226
273
|
|
|
227
274
|
# ----- panel renderers ---------------------------------------------------
|
|
228
275
|
|
|
276
|
+
def set_project_filter(self, project: Any) -> None:
|
|
277
|
+
"""Called by the app when the user toggles the project filter (key 'p')."""
|
|
278
|
+
self._project_filter = project
|
|
279
|
+
self._render_active(self._state)
|
|
280
|
+
self._render_worktrees(self._state)
|
|
281
|
+
|
|
282
|
+
def _session_matches_filter(self, sess: dict[str, Any]) -> bool:
|
|
283
|
+
"""Match a session to the active project filter, tolerating both
|
|
284
|
+
absolute paths and Claude Code's encoded dir form.
|
|
285
|
+
|
|
286
|
+
Claude Code stores sessions under ~/.claude/projects/<encoded>/...
|
|
287
|
+
where <encoded> is the absolute project path with '/' replaced by
|
|
288
|
+
'-' (e.g. -home-ogu-theY4NN-harness). The daemon's session.project
|
|
289
|
+
field carries this encoded form. The filter target is an absolute
|
|
290
|
+
Path. We compare both:
|
|
291
|
+
- absolute → encoded (replace '/' with '-')
|
|
292
|
+
- encoded → basename match (last segment)
|
|
293
|
+
"""
|
|
294
|
+
flt = getattr(self, "_project_filter", None)
|
|
295
|
+
if not flt:
|
|
296
|
+
return True
|
|
297
|
+
proj = (sess.get("project") or "").rstrip("/")
|
|
298
|
+
if not proj:
|
|
299
|
+
return False
|
|
300
|
+
target_abs = str(flt).rstrip("/")
|
|
301
|
+
# Encoded form: leading dash + path with '/' → '-'.
|
|
302
|
+
target_encoded = target_abs.replace("/", "-")
|
|
303
|
+
if proj == target_abs or proj == target_encoded:
|
|
304
|
+
return True
|
|
305
|
+
# Last-segment fallback (covers both encoded and absolute forms).
|
|
306
|
+
return proj.rsplit("-", 1)[-1] == target_abs.rsplit("/", 1)[-1]
|
|
307
|
+
|
|
229
308
|
def _render_active(self, state: dict[str, Any]) -> None:
|
|
230
309
|
try:
|
|
231
310
|
panel = self.query_one("#live-active", Static)
|
|
@@ -233,9 +312,13 @@ class LiveTab(Container):
|
|
|
233
312
|
return
|
|
234
313
|
sessions = (state or {}).get("sessions") or {}
|
|
235
314
|
text = Text()
|
|
236
|
-
|
|
315
|
+
flt = getattr(self, "_project_filter", None)
|
|
316
|
+
title = "ACTIVE" if not flt else f"ACTIVE · {str(flt).split('/')[-1]} only (p to toggle)"
|
|
317
|
+
text.append(title + "\n", style="bold dim")
|
|
237
318
|
any_agent = False
|
|
238
319
|
for sid, sess in sessions.items():
|
|
320
|
+
if not self._session_matches_filter(sess):
|
|
321
|
+
continue
|
|
239
322
|
for name, ag in (sess.get("agents_active") or {}).items():
|
|
240
323
|
any_agent = True
|
|
241
324
|
started = (ag.get("started") or "")[11:19]
|
|
@@ -245,6 +328,9 @@ class LiveTab(Container):
|
|
|
245
328
|
style = _style_for_type("agent_spawn") if status == "running" else "dim"
|
|
246
329
|
text.append(f"{dot} ", style=style)
|
|
247
330
|
text.append(f"{name:14}", style="bold")
|
|
331
|
+
role = _role_for(name)
|
|
332
|
+
if role:
|
|
333
|
+
text.append(f" · {role:18}", style="dim")
|
|
248
334
|
text.append(f" {started} {last_tool}\n", style="dim")
|
|
249
335
|
if not any_agent:
|
|
250
336
|
text.append("(no active agents)\n", style="dim italic")
|
|
@@ -295,6 +381,20 @@ class LiveTab(Container):
|
|
|
295
381
|
text.append(f"{nodes:>6} nodes\n", style="white")
|
|
296
382
|
if not cognee:
|
|
297
383
|
text.append("(cognee not yet probed)\n", style="dim italic")
|
|
384
|
+
g = (state or {}).get("graphify") or {}
|
|
385
|
+
if g:
|
|
386
|
+
nodes = g.get("nodes")
|
|
387
|
+
edges = g.get("edges")
|
|
388
|
+
scans = g.get("scans") or 0
|
|
389
|
+
queries = g.get("queries") or 0
|
|
390
|
+
text.append("● ", style="#00D4AA")
|
|
391
|
+
text.append(f"{'graphify':12} ", style="bold")
|
|
392
|
+
if nodes is not None:
|
|
393
|
+
text.append(f"{nodes:>6} nodes", style="white")
|
|
394
|
+
if edges is not None:
|
|
395
|
+
text.append(f" / {edges:,} edges", style="dim")
|
|
396
|
+
text.append("\n")
|
|
397
|
+
text.append(f" {scans} scans · {queries} queries\n", style="dim")
|
|
298
398
|
mcp = (state or {}).get("mcp_servers") or {}
|
|
299
399
|
if mcp:
|
|
300
400
|
text.append("\nMCP\n", style="bold dim")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Org tab — MISHKAN organisation reference.
|
|
2
|
+
|
|
3
|
+
Read-only browser of the 45-agent org chart. Tree on the left
|
|
4
|
+
(groups → agents), details panel on the right (role + description +
|
|
5
|
+
source). Static data: loaded from org.json via org_data.load_org(),
|
|
6
|
+
never depends on the bus.
|
|
7
|
+
|
|
8
|
+
Purpose: small recalls until the org is memorised.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from textual.app import ComposeResult
|
|
16
|
+
from textual.containers import Container, Horizontal
|
|
17
|
+
from textual.widgets import Static, Tree
|
|
18
|
+
|
|
19
|
+
from ..org_data import load_org as _load_org
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrgTab(Container):
|
|
23
|
+
"""Org reference tab."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, **kwargs) -> None:
|
|
26
|
+
super().__init__(**kwargs)
|
|
27
|
+
self._org = _load_org()
|
|
28
|
+
self._selected: tuple[str, str] | None = None # (group_id, alias)
|
|
29
|
+
|
|
30
|
+
def compose(self) -> ComposeResult:
|
|
31
|
+
with Horizontal(id="org-row"):
|
|
32
|
+
yield Tree("MISHKAN", id="org-tree")
|
|
33
|
+
with Container(id="org-detail"):
|
|
34
|
+
yield Static("(select an agent)", id="org-detail-body")
|
|
35
|
+
|
|
36
|
+
def on_mount(self) -> None:
|
|
37
|
+
self._build_tree()
|
|
38
|
+
|
|
39
|
+
def _build_tree(self) -> None:
|
|
40
|
+
try:
|
|
41
|
+
tree = self.query_one("#org-tree", Tree)
|
|
42
|
+
except Exception:
|
|
43
|
+
return
|
|
44
|
+
tree.root.expand()
|
|
45
|
+
for grp in self._org.get("groups", []):
|
|
46
|
+
label = Text()
|
|
47
|
+
label.append(grp["label"], style="bold")
|
|
48
|
+
domain = grp.get("domain")
|
|
49
|
+
if domain:
|
|
50
|
+
label.append(f" · {domain}", style="#00D4AA")
|
|
51
|
+
label.append(f" ({len(grp.get('agents', []))})", style="dim")
|
|
52
|
+
node = tree.root.add(label, data={"kind": "group", "id": grp["id"]})
|
|
53
|
+
node.expand()
|
|
54
|
+
for ag in grp.get("agents", []):
|
|
55
|
+
al = Text()
|
|
56
|
+
al.append("● ", style="#00D4AA")
|
|
57
|
+
al.append(ag["alias"].capitalize(), style="bold")
|
|
58
|
+
al.append(f" · {ag['role']}", style="dim")
|
|
59
|
+
node.add_leaf(al, data={"kind": "agent", "group": grp["id"], "alias": ag["alias"]})
|
|
60
|
+
|
|
61
|
+
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
|
|
62
|
+
data = event.node.data or {}
|
|
63
|
+
kind = data.get("kind")
|
|
64
|
+
if kind == "agent":
|
|
65
|
+
self._render_agent_detail(data["group"], data["alias"])
|
|
66
|
+
elif kind == "group":
|
|
67
|
+
self._render_group_detail(data["id"])
|
|
68
|
+
|
|
69
|
+
def _render_group_detail(self, gid: str) -> None:
|
|
70
|
+
try:
|
|
71
|
+
panel = self.query_one("#org-detail-body", Static)
|
|
72
|
+
except Exception:
|
|
73
|
+
return
|
|
74
|
+
grp = next((g for g in self._org.get("groups", []) if g["id"] == gid), None)
|
|
75
|
+
if not grp:
|
|
76
|
+
return
|
|
77
|
+
t = Text()
|
|
78
|
+
t.append(grp["label"], style="bold #B794F4")
|
|
79
|
+
if grp.get("domain"):
|
|
80
|
+
t.append(f" · {grp['domain']}", style="bold #00D4AA")
|
|
81
|
+
t.append("\n")
|
|
82
|
+
if grp.get("hebrew"):
|
|
83
|
+
t.append(grp["hebrew"], style="cyan")
|
|
84
|
+
if grp.get("hebrew_meaning"):
|
|
85
|
+
t.append(f" — {grp['hebrew_meaning']}", style="dim italic")
|
|
86
|
+
t.append("\n")
|
|
87
|
+
t.append("\n")
|
|
88
|
+
if grp.get("mission"):
|
|
89
|
+
t.append("mission\n", style="dim")
|
|
90
|
+
t.append(grp["mission"], style="white")
|
|
91
|
+
t.append("\n\n")
|
|
92
|
+
if grp.get("charter"):
|
|
93
|
+
t.append("charter\n", style="dim")
|
|
94
|
+
t.append(grp["charter"], style="white")
|
|
95
|
+
t.append("\n\n")
|
|
96
|
+
if grp.get("relationships"):
|
|
97
|
+
t.append("relationships\n", style="dim")
|
|
98
|
+
t.append(grp["relationships"], style="white")
|
|
99
|
+
t.append("\n\n")
|
|
100
|
+
t.append(f"{len(grp.get('agents', []))} agents — select one for role detail.", style="dim italic")
|
|
101
|
+
panel.update(t)
|
|
102
|
+
|
|
103
|
+
def _render_agent_detail(self, gid: str, alias: str) -> None:
|
|
104
|
+
try:
|
|
105
|
+
panel = self.query_one("#org-detail-body", Static)
|
|
106
|
+
except Exception:
|
|
107
|
+
return
|
|
108
|
+
grp = next((g for g in self._org.get("groups", []) if g["id"] == gid), None)
|
|
109
|
+
if not grp:
|
|
110
|
+
return
|
|
111
|
+
ag = next((a for a in grp.get("agents", []) if a["alias"] == alias), None)
|
|
112
|
+
if not ag:
|
|
113
|
+
return
|
|
114
|
+
t = Text()
|
|
115
|
+
t.append(ag["alias"].capitalize(), style="bold #B794F4")
|
|
116
|
+
t.append(f" · {grp['label']}", style="bold")
|
|
117
|
+
if grp.get("domain"):
|
|
118
|
+
t.append(f" · {grp['domain']}", style="#00D4AA")
|
|
119
|
+
t.append("\n\n")
|
|
120
|
+
t.append("role ", style="dim")
|
|
121
|
+
t.append(ag["role"], style="cyan")
|
|
122
|
+
t.append("\n")
|
|
123
|
+
if ag.get("short"):
|
|
124
|
+
t.append("short ", style="dim")
|
|
125
|
+
t.append(ag["short"], style="white")
|
|
126
|
+
t.append("\n")
|
|
127
|
+
t.append("source ", style="dim")
|
|
128
|
+
t.append(ag.get("source", "—"), style="dim italic")
|
|
129
|
+
t.append("\n\n")
|
|
130
|
+
t.append(ag.get("description", ""), style="white")
|
|
131
|
+
panel.update(t)
|
|
132
|
+
|
|
133
|
+
# ----- API expected by app.py (no-op since static data) -----------------
|
|
134
|
+
|
|
135
|
+
def apply_snapshot(self, state: dict[str, Any]) -> None:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
def apply_event(self, ev: dict[str, Any]) -> None:
|
|
139
|
+
return
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Tab 8 — Skills.
|
|
2
|
+
|
|
3
|
+
3-column browser of every installed skill on the user's Claude Code
|
|
4
|
+
instance, with cross-reference to the CTO decisions (ADRs) where each
|
|
5
|
+
skill is mentioned.
|
|
6
|
+
|
|
7
|
+
Layout:
|
|
8
|
+
- LEFT (28%) : tree grouped by origin (MISHKAN / user / plugin)
|
|
9
|
+
then by category. Selecting a leaf loads detail.
|
|
10
|
+
- CENTER (44%): selected skill's full metadata — origin, source path,
|
|
11
|
+
category, description, triggers ("Use when..." lines).
|
|
12
|
+
- RIGHT (28%) : ADRs that mention this skill, with section bodies
|
|
13
|
+
truncated for at-a-glance reading.
|
|
14
|
+
|
|
15
|
+
Data via skills_data.load_skills() (prefers the indexer output if
|
|
16
|
+
Bezalel's index.json exists; otherwise scans paths directly).
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from textual.app import ComposeResult
|
|
25
|
+
from textual.containers import Container, Horizontal
|
|
26
|
+
from textual.widgets import Static, Tree
|
|
27
|
+
|
|
28
|
+
from ..skills_data import load_skills
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SkillsTab(Container):
|
|
32
|
+
DEFAULT_CSS = ""
|
|
33
|
+
|
|
34
|
+
def __init__(self, **kwargs) -> None:
|
|
35
|
+
super().__init__(**kwargs)
|
|
36
|
+
self._skills: list[dict[str, Any]] = []
|
|
37
|
+
self._selected_name: str | None = None
|
|
38
|
+
|
|
39
|
+
def compose(self) -> ComposeResult:
|
|
40
|
+
with Horizontal(id="skills-row"):
|
|
41
|
+
yield Tree("Skills", id="skills-tree")
|
|
42
|
+
with Container(id="skills-detail"):
|
|
43
|
+
yield Static("", id="skills-detail-body")
|
|
44
|
+
with Container(id="skills-adrs"):
|
|
45
|
+
yield Static("", id="skills-adrs-body")
|
|
46
|
+
|
|
47
|
+
def on_mount(self) -> None:
|
|
48
|
+
try:
|
|
49
|
+
self._skills = load_skills()
|
|
50
|
+
except Exception:
|
|
51
|
+
self._skills = []
|
|
52
|
+
self._rebuild_tree()
|
|
53
|
+
self._render_detail()
|
|
54
|
+
self._render_adrs()
|
|
55
|
+
|
|
56
|
+
def _rebuild_tree(self) -> None:
|
|
57
|
+
try:
|
|
58
|
+
tree = self.query_one("#skills-tree", Tree)
|
|
59
|
+
except Exception:
|
|
60
|
+
return
|
|
61
|
+
tree.clear()
|
|
62
|
+
root = tree.root
|
|
63
|
+
root.expand()
|
|
64
|
+
# Group origin → category → entries.
|
|
65
|
+
groups: dict[str, dict[str, list[dict[str, Any]]]] = defaultdict(
|
|
66
|
+
lambda: defaultdict(list))
|
|
67
|
+
for s in self._skills:
|
|
68
|
+
groups[s.get("origin") or "?"][s.get("category") or "?"].append(s)
|
|
69
|
+
origin_labels = {
|
|
70
|
+
"mishkan": "MISHKAN craft",
|
|
71
|
+
"user": "Community",
|
|
72
|
+
"plugin": "Plugins",
|
|
73
|
+
"project": "Project-local",
|
|
74
|
+
"builtin": "Built-in",
|
|
75
|
+
}
|
|
76
|
+
# Render in fixed order so MISHKAN is always first.
|
|
77
|
+
for origin in ("mishkan", "user", "plugin", "project", "builtin"):
|
|
78
|
+
if origin not in groups:
|
|
79
|
+
continue
|
|
80
|
+
cats = groups[origin]
|
|
81
|
+
total = sum(len(v) for v in cats.values())
|
|
82
|
+
label = Text()
|
|
83
|
+
label.append(origin_labels.get(origin, origin), style="bold")
|
|
84
|
+
label.append(f" ({total})", style="dim")
|
|
85
|
+
on = root.add(label, data={"kind": "origin"})
|
|
86
|
+
on.expand()
|
|
87
|
+
for cat in sorted(cats.keys()):
|
|
88
|
+
entries = cats[cat]
|
|
89
|
+
cl = Text()
|
|
90
|
+
cl.append(cat, style="bold #00D4AA")
|
|
91
|
+
cl.append(f" ({len(entries)})", style="dim")
|
|
92
|
+
cn = on.add(cl, data={"kind": "category"})
|
|
93
|
+
for entry in sorted(entries, key=lambda e: e.get("name") or ""):
|
|
94
|
+
sl = Text()
|
|
95
|
+
sl.append("● ", style="#B794F4")
|
|
96
|
+
sl.append(entry.get("name") or "?", style="white")
|
|
97
|
+
if entry.get("adrs"):
|
|
98
|
+
sl.append(f" · {','.join(entry['adrs'])}", style="#F6AD55 italic")
|
|
99
|
+
cn.add_leaf(sl, data={"kind": "skill", "name": entry.get("name")})
|
|
100
|
+
|
|
101
|
+
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
|
|
102
|
+
data = event.node.data or {}
|
|
103
|
+
if data.get("kind") == "skill":
|
|
104
|
+
self._selected_name = data.get("name")
|
|
105
|
+
self._render_detail()
|
|
106
|
+
self._render_adrs()
|
|
107
|
+
|
|
108
|
+
def _render_detail(self) -> None:
|
|
109
|
+
try:
|
|
110
|
+
panel = self.query_one("#skills-detail-body", Static)
|
|
111
|
+
except Exception:
|
|
112
|
+
return
|
|
113
|
+
if not self._selected_name:
|
|
114
|
+
panel.update(Text("(select a skill on the left)\n\n"
|
|
115
|
+
f"{len(self._skills)} skills installed",
|
|
116
|
+
style="dim italic"))
|
|
117
|
+
return
|
|
118
|
+
entry = next((s for s in self._skills if s.get("name") == self._selected_name), None)
|
|
119
|
+
if not entry:
|
|
120
|
+
panel.update(Text("(skill no longer in index)", style="dim italic"))
|
|
121
|
+
return
|
|
122
|
+
t = Text()
|
|
123
|
+
t.append(entry["name"], style="bold #B794F4")
|
|
124
|
+
t.append("\n\n")
|
|
125
|
+
t.append("origin ", style="dim")
|
|
126
|
+
t.append(entry.get("origin") or "?", style="cyan")
|
|
127
|
+
t.append("\n")
|
|
128
|
+
t.append("category ", style="dim")
|
|
129
|
+
t.append(entry.get("category") or "?", style="cyan")
|
|
130
|
+
t.append("\n")
|
|
131
|
+
t.append("source ", style="dim")
|
|
132
|
+
t.append(str(entry.get("source_path") or "?")[-60:], style="dim italic")
|
|
133
|
+
t.append("\n\n")
|
|
134
|
+
desc = entry.get("description") or "(no description)"
|
|
135
|
+
t.append("description\n", style="bold dim")
|
|
136
|
+
t.append(desc + "\n", style="white")
|
|
137
|
+
triggers = entry.get("triggers") or []
|
|
138
|
+
if triggers:
|
|
139
|
+
t.append("\nuse when\n", style="bold dim")
|
|
140
|
+
for tr in triggers[:4]:
|
|
141
|
+
t.append(f" · {tr[:80]}\n", style="white")
|
|
142
|
+
# Frontmatter dump for the curious.
|
|
143
|
+
fm = entry.get("frontmatter") or {}
|
|
144
|
+
if fm:
|
|
145
|
+
t.append("\nfrontmatter\n", style="bold dim")
|
|
146
|
+
for k, v in list(fm.items())[:6]:
|
|
147
|
+
if k in ("name", "description"):
|
|
148
|
+
continue
|
|
149
|
+
t.append(f" {k}: ", style="dim")
|
|
150
|
+
t.append(str(v)[:60] + "\n", style="white")
|
|
151
|
+
panel.update(t)
|
|
152
|
+
|
|
153
|
+
def _render_adrs(self) -> None:
|
|
154
|
+
try:
|
|
155
|
+
panel = self.query_one("#skills-adrs-body", Static)
|
|
156
|
+
except Exception:
|
|
157
|
+
return
|
|
158
|
+
t = Text()
|
|
159
|
+
t.append("CTO decisions\n", style="bold #B794F4")
|
|
160
|
+
if not self._selected_name:
|
|
161
|
+
t.append("\n(select a skill to see linked ADRs)", style="dim italic")
|
|
162
|
+
panel.update(t)
|
|
163
|
+
return
|
|
164
|
+
entry = next((s for s in self._skills if s.get("name") == self._selected_name), None)
|
|
165
|
+
adrs = (entry or {}).get("adrs") or []
|
|
166
|
+
if not adrs:
|
|
167
|
+
t.append("\n(no ADR mentions this skill)\n", style="dim italic")
|
|
168
|
+
t.append("\nADRs live in docs/design/MISHKAN_decisions.md", style="dim")
|
|
169
|
+
panel.update(t)
|
|
170
|
+
return
|
|
171
|
+
t.append(f"\n{len(adrs)} ADR mention(s):\n\n", style="dim")
|
|
172
|
+
for adr_id in adrs:
|
|
173
|
+
t.append(f" ● {adr_id}\n", style="#F6AD55 bold")
|
|
174
|
+
t.append("\nFull text:\n", style="bold dim")
|
|
175
|
+
t.append(" docs/design/MISHKAN_decisions.md\n", style="dim italic")
|
|
176
|
+
panel.update(t)
|
|
177
|
+
|
|
178
|
+
# ----- API expected by app.py -------------------------------------------
|
|
179
|
+
|
|
180
|
+
def apply_snapshot(self, state: dict[str, Any]) -> None:
|
|
181
|
+
return # static data, no daemon dependency
|
|
182
|
+
|
|
183
|
+
def apply_event(self, ev: dict[str, Any]) -> None:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
def set_project_filter(self, project: Any) -> None:
|
|
187
|
+
return
|