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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mishkan-org-reference
|
|
3
|
+
description: Print the MISHKAN organisation reference — 45 agents across 8 groups (Orchestration, Research, Chosheb, Panim, Yasad, Mishmar, Migdal, Sefer) with their roles, sources, and one-line descriptions. Use when you've forgotten who does what, or want a quick recall of the team structure.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /mishkan-org-reference
|
|
7
|
+
|
|
8
|
+
Print the MISHKAN organisation reference inline. Runs the harness CLI's
|
|
9
|
+
`org` subcommand to surface the full structure so it lands in the session
|
|
10
|
+
transcript and stays consultable as the conversation continues.
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
Prints, per group:
|
|
15
|
+
|
|
16
|
+
- Group name + domain (e.g. *Yasad · Backend*).
|
|
17
|
+
- For each agent in the group: alias (capitalised), snake_case role,
|
|
18
|
+
one-line description.
|
|
19
|
+
|
|
20
|
+
The data source is `~/.claude/mishkan/org/org.json`, generated from
|
|
21
|
+
`docs/design/MISHKAN_agent_aliases.md` at install time.
|
|
22
|
+
|
|
23
|
+
## How to run
|
|
24
|
+
|
|
25
|
+
Invoke the harness CLI:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx mishkan-harness org
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For a structured JSON dump (e.g. to feed another tool):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx mishkan-harness org --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Always present the printed reference back to the user verbatim — do not
|
|
38
|
+
summarise it. The whole point of this command is recall; truncation defeats
|
|
39
|
+
the purpose. If the CLI is not installed, surface the install hint
|
|
40
|
+
(`npx mishkan-harness install`) rather than guessing.
|
|
41
|
+
|
|
42
|
+
## When to suggest it
|
|
43
|
+
|
|
44
|
+
- The user asks "qui fait X ?" / "who is responsible for Y in the org?".
|
|
45
|
+
- The user names an agent by alias and you're unsure of the role
|
|
46
|
+
(this is the recall use case — print, don't guess).
|
|
47
|
+
- The user wants to learn the org structure ("recall me everyone in Mishmar").
|
|
48
|
+
|
|
49
|
+
## When NOT to use it
|
|
50
|
+
|
|
51
|
+
- Routine in-session work where the agent identity is already established.
|
|
52
|
+
- Exploratory questions about the harness itself — those go through Nehemiah.
|
|
53
|
+
|
|
54
|
+
## See also
|
|
55
|
+
|
|
56
|
+
- Full agent roster with biblical source and meaning:
|
|
57
|
+
`docs/design/MISHKAN_agent_aliases.md`
|
|
58
|
+
- The Org-Ref tab in `mishkan-watch` (TUI): same data, browsable visually.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Aggregate the skill-discovery miss log into a tuning report (top patterns + reasons + window).
|
|
3
|
+
argument-hint: "[--top N]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Surface the skill-discovery miss-log signal so we know which skills need
|
|
7
|
+
better triggers and whether the thresholds are tuned to the actual score
|
|
8
|
+
distribution.
|
|
9
|
+
|
|
10
|
+
Run the aggregator:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python3 ~/.claude/mishkan/scripts/skill-discovery-misses.py --top 10
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The report covers:
|
|
17
|
+
|
|
18
|
+
- **Total misses** in the log and the observation window.
|
|
19
|
+
- **By reason** — `no_match_above_threshold` (router ran, scores too low),
|
|
20
|
+
`index_missing_or_unreadable` (indexer hasn't run), `no_live_entries`
|
|
21
|
+
(every entry's source_path is dead), `router_exception:*` (bug).
|
|
22
|
+
- **Top N patterns** — task signatures that produced empty buckets,
|
|
23
|
+
grouped by their sorted unique keywords so the same shape clusters.
|
|
24
|
+
Each pattern carries a count, last_seen, and an example task.
|
|
25
|
+
|
|
26
|
+
Two tuning levers, in order:
|
|
27
|
+
|
|
28
|
+
1. **Description tuning** (preferred). If the same pattern shows up in the
|
|
29
|
+
top-N and a skill clearly *should* have caught it, enrich that skill's
|
|
30
|
+
`description` so the keywords used in the task hit. This is the
|
|
31
|
+
D-011 §"Phase 1 → Phase 2 path" rule: tune descriptions before
|
|
32
|
+
threshold (description fixes are free; threshold changes affect every
|
|
33
|
+
future routing).
|
|
34
|
+
2. **Threshold tuning** (only after 2 sprints of stable distribution).
|
|
35
|
+
Lower `--threshold-high` if real matches are being demoted to
|
|
36
|
+
`should_consider`; raise it if `must_load` is over-firing on
|
|
37
|
+
marginally-relevant skills.
|
|
38
|
+
|
|
39
|
+
Surface the report verbatim, then call out:
|
|
40
|
+
|
|
41
|
+
- Any pattern with count ≥ 5 — that's a recurring miss worth a description
|
|
42
|
+
edit.
|
|
43
|
+
- A miss rate (`total / sessions-since-first-seen`) over ~10% — the index
|
|
44
|
+
or trigger surface is the wrong shape for the workload.
|
|
45
|
+
- `router_exception:*` reasons — those are bugs; escalate to Bezalel.
|
|
46
|
+
|
|
47
|
+
If the miss log is empty, say so — that is success, not nothing-to-report.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rebuild the universal skill-discovery index by rescanning every installed skill root.
|
|
3
|
+
argument-hint: "(no arguments)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Rebuild the universal skill-discovery index.
|
|
7
|
+
|
|
8
|
+
Run the indexer in manual-rebuild mode:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python3 ~/.claude/mishkan/scripts/skill-discovery-indexer.py --manual
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The indexer scans, in precedence order:
|
|
15
|
+
|
|
16
|
+
1. `~/.claude/mishkan/skills/` (origin = mishkan)
|
|
17
|
+
2. `~/.claude/skills/` (origin = user)
|
|
18
|
+
3. `~/.claude/plugins/*/skills/` (origin = plugin)
|
|
19
|
+
4. `<current-repo>/.claude/skills/` (origin = project)
|
|
20
|
+
|
|
21
|
+
It writes `~/.claude/mishkan/skill-discovery/index.json` and prints a summary
|
|
22
|
+
of how many entries were indexed and how many name collisions were recorded.
|
|
23
|
+
|
|
24
|
+
After the rebuild, surface:
|
|
25
|
+
|
|
26
|
+
- The total count.
|
|
27
|
+
- The number of collisions (if non-zero, mention they live in
|
|
28
|
+
`index.json.meta.collisions`).
|
|
29
|
+
- Any errors logged to `~/.claude/mishkan/skill-discovery/indexer-errors.jsonl`
|
|
30
|
+
(read the last 5 lines if the file exists).
|
|
31
|
+
|
|
32
|
+
If the rebuild fails (non-zero exit, parse error), do **not** retry blindly —
|
|
33
|
+
surface the error to Y4NN.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Surface the most relevant installed skills for the current task (advisory; you decide what to load).
|
|
3
|
+
argument-hint: "[optional task description; defaults to the current message context]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the **skill-discovery** layer against the current task.
|
|
7
|
+
|
|
8
|
+
Task (from arguments, fall back to the active message): $ARGUMENTS
|
|
9
|
+
|
|
10
|
+
Steps:
|
|
11
|
+
|
|
12
|
+
1. If a task description was passed as `$ARGUMENTS`, use it verbatim.
|
|
13
|
+
Otherwise, write a one-paragraph summary of what the user is trying to
|
|
14
|
+
do right now.
|
|
15
|
+
2. Invoke the router:
|
|
16
|
+
```bash
|
|
17
|
+
python3 ~/.claude/mishkan/scripts/skill-discovery-router.py \
|
|
18
|
+
--task "<task summary>"
|
|
19
|
+
```
|
|
20
|
+
3. Parse the JSON response. Surface the three buckets to the user with this
|
|
21
|
+
shape:
|
|
22
|
+
- **must_load** — name, one-line description, origin tag
|
|
23
|
+
- **should_consider** — name, one-line description, origin tag
|
|
24
|
+
- **adjacent** — name only (cheaper context)
|
|
25
|
+
4. If `stale_rebuild_needed: true`, tell the user to run
|
|
26
|
+
`/mishkan-skills-reindex`.
|
|
27
|
+
5. If buckets are all empty, surface that fact — the miss is already logged
|
|
28
|
+
to `~/.claude/mishkan/skill-discovery/misses.jsonl` for tuning.
|
|
29
|
+
6. **Do not auto-load** any non-`mishkan` skill for a stateful operation;
|
|
30
|
+
ask Y4NN first. Generative work may be loaded after reading the skill's
|
|
31
|
+
description.
|
|
32
|
+
|
|
33
|
+
This command is advisory. The actual loading decision stays with the model
|
|
34
|
+
and with Y4NN. Apply the skill-discovery skill (`~/.claude/mishkan/skills/skill-discovery/`)
|
|
35
|
+
for interpretation rules.
|
|
@@ -14,7 +14,9 @@ Then run:
|
|
|
14
14
|
ADRs, API docs, runbooks, team docs updated in `docs/`.
|
|
15
15
|
5. Resolved research + decisions promoted to the Cognee project graph
|
|
16
16
|
(**cognee-promote** skill) — gated by Nehemiah + Bezalel.
|
|
17
|
-
6. Observability aggregation runs
|
|
17
|
+
6. Observability aggregation runs (`bash {{MISHKAN}}/scripts/observability-aggregate.sh`)
|
|
18
|
+
to produce per-tool and per-outcome counts plus per-session activity from
|
|
19
|
+
`logs/*.jsonl`. Improvement-layer queries refresh from the produced summary.
|
|
18
20
|
7. Update `./CLAUDE.md` to the next sprint (S+1) and reset milestone.
|
|
19
21
|
|
|
20
22
|
Stateful operations stop at Y4NN's hands. Reporters surface structured summaries
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# MISHKAN PreToolUse — graphify-first advisory (Phase 2: advisory injection).
|
|
3
|
+
#
|
|
4
|
+
# Per D-009 + amendment 2026-06-07 (MISHKAN_decisions.md):
|
|
5
|
+
#
|
|
6
|
+
# When a structural Read or Grep fires on a source file, this hook does
|
|
7
|
+
# TWO things:
|
|
8
|
+
# 1. Emit a hook_fire event on the bus (telemetry preserved from
|
|
9
|
+
# Phase 1; the Knowledge tab keeps its Graphify activity counter).
|
|
10
|
+
# 2. Inject an advisory block via hookSpecificOutput.additionalContext
|
|
11
|
+
# suggesting `graphify query` as the cheaper, structurally precise
|
|
12
|
+
# alternative. The hook never blocks; the agent is free to ignore
|
|
13
|
+
# the advisory and proceed with the Read/Grep.
|
|
14
|
+
#
|
|
15
|
+
# Skill-discovery's PreToolUse skill-routing hook can't be relied on
|
|
16
|
+
# alone to push graphify because its router scores skills against the
|
|
17
|
+
# initial Task prompt — "implement payment flow" matches
|
|
18
|
+
# implementation skills, not "structural-question" skills like
|
|
19
|
+
# graphify-query-craft. The runtime nudge here closes that gap: it
|
|
20
|
+
# fires at the exact moment the agent is about to do a Read/Grep that
|
|
21
|
+
# Graphify could have answered, regardless of what was injected at
|
|
22
|
+
# dispatch time.
|
|
23
|
+
#
|
|
24
|
+
# Triggers:
|
|
25
|
+
# - Read on file_path ending in a source extension (.py .ts .tsx .js
|
|
26
|
+
# .jsx .mjs .cjs .go .rs .java .php .rb)
|
|
27
|
+
# - Grep on a bare-identifier pattern (^[A-Za-z_][A-Za-z0-9_]*$)
|
|
28
|
+
#
|
|
29
|
+
# NOT triggers (configs / markdown / YAML / regex Grep patterns). Per
|
|
30
|
+
# D-009 §2.
|
|
31
|
+
#
|
|
32
|
+
# Performance contract: <= 50 ms p95. Bash hot path keeps the cold-start
|
|
33
|
+
# below the Python alternative (the D-009 §6 unknown). No subprocess
|
|
34
|
+
# beyond jq; fail-open everywhere; never blocks a tool call. If the
|
|
35
|
+
# project has no graphify-out/ directory, the advisory is suppressed
|
|
36
|
+
# (no point recommending a tool the project hasn't initialised).
|
|
37
|
+
|
|
38
|
+
set -uo pipefail
|
|
39
|
+
|
|
40
|
+
# jq absent -> noop. Observability never breaks a tool call.
|
|
41
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
42
|
+
|
|
43
|
+
# Source the observability bus (fail-open if not yet installed).
|
|
44
|
+
MISHKAN_HOME_RES="${MISHKAN_HOME:-$HOME/.claude/mishkan}"
|
|
45
|
+
# shellcheck disable=SC1091
|
|
46
|
+
source "${MISHKAN_HOME_RES}/observability/bus.sh" 2>/dev/null || exit 0
|
|
47
|
+
|
|
48
|
+
INPUT="$(cat)"
|
|
49
|
+
tool="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
50
|
+
session="$(printf '%s' "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null)"
|
|
51
|
+
|
|
52
|
+
# Structural-Read detection.
|
|
53
|
+
target=""
|
|
54
|
+
structural=0
|
|
55
|
+
case "$tool" in
|
|
56
|
+
Read)
|
|
57
|
+
path="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)"
|
|
58
|
+
lc_path="$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')"
|
|
59
|
+
case "$lc_path" in
|
|
60
|
+
*.py|*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.go|*.rs|*.java|*.php|*.rb)
|
|
61
|
+
structural=1
|
|
62
|
+
target="$path"
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
;;
|
|
66
|
+
Grep)
|
|
67
|
+
pattern="$(printf '%s' "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)"
|
|
68
|
+
# Bare identifier only: ^[A-Za-z_][A-Za-z0-9_]*$
|
|
69
|
+
if [ -n "$pattern" ] && printf '%s' "$pattern" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$'; then
|
|
70
|
+
structural=1
|
|
71
|
+
target="$pattern"
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
[ "$structural" = 1 ] || exit 0
|
|
77
|
+
|
|
78
|
+
# Emit the telemetry event (Phase 1 contract preserved).
|
|
79
|
+
if command -v bus_emit >/dev/null 2>&1; then
|
|
80
|
+
short_target="$(printf '%s' "$target" | cut -c1-120)"
|
|
81
|
+
payload="$(jq -cn --arg t "$short_target" \
|
|
82
|
+
'{hook:"knowledge-route", decision:"ok", phase:2,
|
|
83
|
+
reason:"structural read/grep detected — advisory injected",
|
|
84
|
+
target:$t}' 2>/dev/null)"
|
|
85
|
+
bus_emit "$session" "hook_fire" "$tool" "completed" "$payload"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Three knowledge stores per D-008 — we mention all three so the agent
|
|
89
|
+
# has the full palette instead of a graphify-only push:
|
|
90
|
+
# 1. Graphify — code structure (who calls X, dependencies, paths)
|
|
91
|
+
# 2. Cognee work — project semantic memory (decisions, conventions)
|
|
92
|
+
# 3. Cognee curated — cross-project reference library (specs, learnings)
|
|
93
|
+
# The agent picks whichever fits its actual question.
|
|
94
|
+
|
|
95
|
+
project_root="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
96
|
+
graphify_available="no"
|
|
97
|
+
[ -d "$project_root/graphify-out" ] && graphify_available="yes"
|
|
98
|
+
|
|
99
|
+
# Gather objective graph signals so the agent has data, not a verdict.
|
|
100
|
+
graph_nodes="?"
|
|
101
|
+
graph_edges="?"
|
|
102
|
+
graph_age=""
|
|
103
|
+
graph_json="$project_root/graphify-out/graph.json"
|
|
104
|
+
if [ -f "$graph_json" ]; then
|
|
105
|
+
graph_nodes="$(jq -r '(.nodes // []) | length' "$graph_json" 2>/dev/null || echo '?')"
|
|
106
|
+
graph_edges="$(jq -r '(.links // .edges // []) | length' "$graph_json" 2>/dev/null || echo '?')"
|
|
107
|
+
scan_mtime="$(stat -c '%Y' "$graph_json" 2>/dev/null || stat -f '%m' "$graph_json" 2>/dev/null || echo 0)"
|
|
108
|
+
now_ts="$(date +%s)"
|
|
109
|
+
age_s=$((now_ts - scan_mtime))
|
|
110
|
+
if [ "$age_s" -lt 3600 ]; then graph_age="$((age_s / 60))min ago"
|
|
111
|
+
elif [ "$age_s" -lt 86400 ]; then graph_age="$((age_s / 3600))h ago"
|
|
112
|
+
else graph_age="$((age_s / 86400))d ago"
|
|
113
|
+
fi
|
|
114
|
+
[ "$age_s" -gt 3600 ] && graph_age="$graph_age (stale — \`/code-graph scan\` to refresh)"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Cognee freshness — use the daemon's poll cache instead of probing
|
|
118
|
+
# directly. The cognee_poll source already does HTTP probes every 30s
|
|
119
|
+
# and writes the latest node counts to a small cache file. Reading the
|
|
120
|
+
# cache stays within the 50ms p95 budget (a probe wouldn't). When the
|
|
121
|
+
# cache is missing or stale, we fall back to "?" — the cognee line in
|
|
122
|
+
# the palette still appears, the agent just doesn't get the count.
|
|
123
|
+
cognee_cache="$HOME/.cache/mishkan/cognee-counts.json"
|
|
124
|
+
cognee_work_nodes="?"
|
|
125
|
+
cognee_curated_nodes="?"
|
|
126
|
+
cognee_freshness=""
|
|
127
|
+
if [ -f "$cognee_cache" ]; then
|
|
128
|
+
cognee_work_nodes="$(jq -r '.work.nodes // "?"' "$cognee_cache" 2>/dev/null || echo '?')"
|
|
129
|
+
cognee_curated_nodes="$(jq -r '.curated.nodes // "?"' "$cognee_cache" 2>/dev/null || echo '?')"
|
|
130
|
+
cog_mtime="$(stat -c '%Y' "$cognee_cache" 2>/dev/null || stat -f '%m' "$cognee_cache" 2>/dev/null || echo 0)"
|
|
131
|
+
cog_age_s=$((now_ts - cog_mtime))
|
|
132
|
+
if [ "$cog_age_s" -lt 120 ]; then
|
|
133
|
+
cognee_freshness="cache fresh ($((cog_age_s))s ago)"
|
|
134
|
+
elif [ "$cog_age_s" -lt 3600 ]; then
|
|
135
|
+
cognee_freshness="cache $((cog_age_s / 60))min old"
|
|
136
|
+
else
|
|
137
|
+
cognee_freshness="cache $((cog_age_s / 3600))h old — daemon may be down"
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
# Helper formatting for the cognee lines in the palette below.
|
|
141
|
+
fmt_cognee_count() {
|
|
142
|
+
case "$1" in
|
|
143
|
+
"?") echo "(node count unknown)" ;;
|
|
144
|
+
0|"0") echo "(empty — nothing ingested yet)" ;;
|
|
145
|
+
*) echo "($1 nodes)" ;;
|
|
146
|
+
esac
|
|
147
|
+
}
|
|
148
|
+
cog_work_hint="$(fmt_cognee_count "$cognee_work_nodes")"
|
|
149
|
+
cog_curated_hint="$(fmt_cognee_count "$cognee_curated_nodes")"
|
|
150
|
+
|
|
151
|
+
# Build the advisory palette. Includes ALL three stores when each can
|
|
152
|
+
# help; per-tool wording so the agent sees a pre-formed command for its
|
|
153
|
+
# actual target/pattern (Grep) or topic (Read).
|
|
154
|
+
# Per-route token cost hints — concrete numbers so the agent sees the
|
|
155
|
+
# trade-off, not just a name. Read scales with file size; graphify and
|
|
156
|
+
# cognee are roughly constant per query.
|
|
157
|
+
GRAPHIFY_COST_HINT="~1.8k tokens"
|
|
158
|
+
COGNEE_COST_HINT="~500 tokens"
|
|
159
|
+
|
|
160
|
+
case "$tool" in
|
|
161
|
+
Read)
|
|
162
|
+
short="$(basename "$target")"
|
|
163
|
+
file_lines="?"
|
|
164
|
+
read_cost_hint="~? tokens"
|
|
165
|
+
if [ -f "$target" ]; then
|
|
166
|
+
file_lines="$(wc -l < "$target" 2>/dev/null || echo '?')"
|
|
167
|
+
if [ "$file_lines" != "?" ] && [ "$file_lines" -gt 0 ]; then
|
|
168
|
+
read_tokens=$((file_lines * 4)) # ~4 tokens / line of source
|
|
169
|
+
if [ "$read_tokens" -lt 200 ]; then
|
|
170
|
+
read_cost_hint="<200 tokens"
|
|
171
|
+
else
|
|
172
|
+
read_cost_hint="~${read_tokens} tokens"
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
if [ "$graphify_available" = "yes" ]; then
|
|
177
|
+
graphify_line="**CODE STRUCTURE** (call graph / dependencies / impact / where used) — \"who calls X\", \"what depends on Y\", \"path between A and B\" → \`graphify query \"<question>\"\` ($GRAPHIFY_COST_HINT). Graph: $graph_nodes nodes / $graph_edges edges, last scan $graph_age."
|
|
178
|
+
else
|
|
179
|
+
graphify_line="**CODE STRUCTURE** → no \`graphify-out/\` in this project (graphify not initialised; run \`graphify update .\` to enable)."
|
|
180
|
+
fi
|
|
181
|
+
advisory="About to Read \`$short\` (~$file_lines lines, $read_cost_hint). MISHKAN has four knowledge surfaces — each answers a different question type (D-008):
|
|
182
|
+
|
|
183
|
+
- $graphify_line
|
|
184
|
+
- **THIS project's MEMORY** — what WE already decided, wrote, resolved (ADRs, runbooks, past sprints, resolved research) → \`mcp__cognee__search\` against the work graph $cog_work_hint ($COGNEE_COST_HINT).
|
|
185
|
+
- **CROSS-PROJECT REFERENCE** — what we learned on OTHER projects, ingested specs and standards, shared read-only library → \`mcp__cognee-curated__search\` $cog_curated_hint ($COGNEE_COST_HINT).
|
|
186
|
+
- **THIS file's literal content** — actual source / docs / config of \`$short\` → Read is the right tool, $read_cost_hint.
|
|
187
|
+
|
|
188
|
+
Pick the surface that matches your actual question."
|
|
189
|
+
;;
|
|
190
|
+
Grep)
|
|
191
|
+
# Verify whether the Grep target is actually a node in the current
|
|
192
|
+
# graph. When yes → graphify is a sure win, we flag it. When no →
|
|
193
|
+
# graphify has nothing on this identifier; don't burn 1.8k on a
|
|
194
|
+
# query whose seed isn't in the graph.
|
|
195
|
+
target_in_graph="unknown"
|
|
196
|
+
if [ "$graphify_available" = "yes" ] && [ -f "$graph_json" ]; then
|
|
197
|
+
# Graphify nodes carry the identifier in `label` (and a normalised
|
|
198
|
+
# form in `norm_label`); `id` is the internal scope-prefixed key
|
|
199
|
+
# ("file_path_segment_identifier") which never matches a bare grep.
|
|
200
|
+
target_in_graph="$(jq -r --arg t "$target" '
|
|
201
|
+
if (.nodes // []) | map((.label // empty), (.norm_label // empty)) | index($t)
|
|
202
|
+
then "yes" else "no" end
|
|
203
|
+
' "$graph_json" 2>/dev/null || echo unknown)"
|
|
204
|
+
fi
|
|
205
|
+
if [ "$graphify_available" = "yes" ]; then
|
|
206
|
+
case "$target_in_graph" in
|
|
207
|
+
yes) graphify_line="**CODE STRUCTURE** (call graph / dependencies / impact) — who calls $target, what depends on it, path between symbols → \`graphify query \"who calls $target\"\` or \`graphify affected $target --depth 2\` ($GRAPHIFY_COST_HINT each). **\`$target\` IS a node in the graph — graphify will return real callers, including indirect ones Grep misses.** Graph: $graph_nodes nodes / $graph_edges edges, last scan $graph_age." ;;
|
|
208
|
+
no) graphify_line="**CODE STRUCTURE** → \`$target\` is NOT a node in the current graph ($graph_nodes nodes / $graph_edges edges, last scan $graph_age). Either truly absent (graphify won't help) or the graph is stale (\`/code-graph scan\` to refresh). Don't burn $GRAPHIFY_COST_HINT on a seedless query." ;;
|
|
209
|
+
*) graphify_line="**CODE STRUCTURE** (call graph / dependencies / impact) — who calls $target, what depends on it → \`graphify query \"who calls $target\"\` or \`graphify affected $target --depth 2\` ($GRAPHIFY_COST_HINT each, catches indirect callers Grep misses). Graph: $graph_nodes nodes / $graph_edges edges, last scan $graph_age." ;;
|
|
210
|
+
esac
|
|
211
|
+
else
|
|
212
|
+
graphify_line="**CODE STRUCTURE** → no \`graphify-out/\` in this project (run \`graphify update .\` to enable; Grep stays valid in the meantime)."
|
|
213
|
+
fi
|
|
214
|
+
advisory="\`Grep $target\` on a bare identifier — MISHKAN has four knowledge surfaces and each answers a different question type (D-008):
|
|
215
|
+
|
|
216
|
+
- $graphify_line
|
|
217
|
+
- **THIS project's MEMORY** — \"why is $target the way it is, who decided it, when, in which sprint\" — our ADRs, runbooks, past research → \`mcp__cognee__search\` with $target as a term $cog_work_hint ($COGNEE_COST_HINT).
|
|
218
|
+
- **CROSS-PROJECT REFERENCE** — \"is $target a standard term elsewhere, what does the spec / curated learning say about it\" → \`mcp__cognee-curated__search\` $cog_curated_hint ($COGNEE_COST_HINT).
|
|
219
|
+
- **LITERAL text occurrences** — \"every line that contains the exact string \`$target\`, regardless of meaning\" → Grep is the right tool.
|
|
220
|
+
|
|
221
|
+
Pick the surface that matches your actual question."
|
|
222
|
+
;;
|
|
223
|
+
esac
|
|
224
|
+
|
|
225
|
+
# Emit hookSpecificOutput.additionalContext via stdout. Claude Code
|
|
226
|
+
# prepends this to the tool call context. Never sets permissionDecision
|
|
227
|
+
# — this is advisory, not gating.
|
|
228
|
+
jq -cn --arg ctx "$advisory" \
|
|
229
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse", additionalContext:$ctx}}' 2>/dev/null
|
|
230
|
+
|
|
231
|
+
exit 0
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# MISHKAN PreToolUse — auto-inject skill-discovery advisory on Task calls
|
|
3
|
+
# (D-011 Phase 2).
|
|
4
|
+
#
|
|
5
|
+
# Fires before Task / Agent (subagent) tool calls. Reads the task prompt
|
|
6
|
+
# from the Claude Code hook stdin payload, runs the universal
|
|
7
|
+
# skill-discovery router in --format injection mode, and returns the
|
|
8
|
+
# resulting compact markdown block via the official PreToolUse
|
|
9
|
+
# additionalContext field — Claude prepends it to the subagent's prompt.
|
|
10
|
+
#
|
|
11
|
+
# Output protocol (Claude Code hooks):
|
|
12
|
+
# {
|
|
13
|
+
# "hookSpecificOutput": {
|
|
14
|
+
# "hookEventName": "PreToolUse",
|
|
15
|
+
# "additionalContext": "## Discovered skills (advisory)\n..."
|
|
16
|
+
# }
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# This is the documented contract for PreToolUse advisory context — the
|
|
20
|
+
# same shape SessionStart uses for additionalContext. We deliberately do
|
|
21
|
+
# NOT set permissionDecision: the router is advisory, never a gate.
|
|
22
|
+
#
|
|
23
|
+
# Performance contract: <= 100 ms p95. The router is a small stdlib-only
|
|
24
|
+
# Python script reading a flat JSON index; cold-start is the dominant cost
|
|
25
|
+
# (~40-60 ms on the harness host) and only fires once per Task call.
|
|
26
|
+
#
|
|
27
|
+
# Hard constraints:
|
|
28
|
+
# - Hard cap 600 tokens of injection (router enforces; we trust it).
|
|
29
|
+
# - Skip injection entirely on empty buckets — do not pollute the
|
|
30
|
+
# subagent prompt with "no skills found" noise.
|
|
31
|
+
# - Fail-open on every error path: no output, exit 0, never block the
|
|
32
|
+
# Task call.
|
|
33
|
+
# - Trust marker preserved: non-mishkan entries already get a
|
|
34
|
+
# "(community)" suffix from the router's injection renderer.
|
|
35
|
+
#
|
|
36
|
+
# Wired in payload/install/settings.hooks.json under PreToolUse with
|
|
37
|
+
# matcher "Task|Agent" (matches the existing model-route entry pattern).
|
|
38
|
+
|
|
39
|
+
set -uo pipefail
|
|
40
|
+
|
|
41
|
+
# jq absent -> noop. The Task call proceeds without advisory injection.
|
|
42
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
43
|
+
# python3 absent -> noop, same reason.
|
|
44
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
45
|
+
|
|
46
|
+
MISHKAN_HOME_RES="${MISHKAN_HOME:-$HOME/.claude/mishkan}"
|
|
47
|
+
ROUTER="${MISHKAN_HOME_RES}/scripts/skill-discovery-router.py"
|
|
48
|
+
[ -f "$ROUTER" ] || exit 0
|
|
49
|
+
|
|
50
|
+
# Source the observability bus (fail-open if not installed).
|
|
51
|
+
# shellcheck disable=SC1091
|
|
52
|
+
source "${MISHKAN_HOME_RES}/observability/bus.sh" 2>/dev/null || true
|
|
53
|
+
|
|
54
|
+
INPUT="$(cat)"
|
|
55
|
+
tool="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
56
|
+
session="$(printf '%s' "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null)"
|
|
57
|
+
|
|
58
|
+
# Belt-and-braces matcher: settings.json already filters to Task|Agent,
|
|
59
|
+
# but if a future Claude Code release widens the matcher we'd be a noop
|
|
60
|
+
# anyway. Better to short-circuit explicitly.
|
|
61
|
+
case "$tool" in
|
|
62
|
+
Task|Agent) ;;
|
|
63
|
+
*) exit 0 ;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
# Extract the subagent's prompt. Task/Agent payloads carry the prompt at
|
|
67
|
+
# .tool_input.prompt; if absent we have nothing to route on.
|
|
68
|
+
prompt="$(printf '%s' "$INPUT" | jq -r '.tool_input.prompt // empty' 2>/dev/null)"
|
|
69
|
+
if [ -z "$prompt" ]; then
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Truncate to a sane size before handing to the router. The router's
|
|
74
|
+
# tokenizer caps usefully around ~500 chars of signal; we cap higher to
|
|
75
|
+
# preserve trailing keywords, but past 4 KB we're just paying cold-start
|
|
76
|
+
# tokens for nothing.
|
|
77
|
+
prompt_trim="$(printf '%s' "$prompt" | head -c 4000)"
|
|
78
|
+
|
|
79
|
+
# Run the router. We pass --format injection so we get the compact
|
|
80
|
+
# markdown block (empty when buckets are empty). Timeout caps the
|
|
81
|
+
# worst-case at 1.5 s — well above the 100 ms p95 budget but a hard floor
|
|
82
|
+
# against a wedged interpreter blocking the Task call.
|
|
83
|
+
block="$(
|
|
84
|
+
timeout 1.5s python3 "$ROUTER" \
|
|
85
|
+
--task "$prompt_trim" \
|
|
86
|
+
--format injection \
|
|
87
|
+
--max-injection-tokens 600 2>/dev/null
|
|
88
|
+
)" || block=""
|
|
89
|
+
|
|
90
|
+
# Empty injection -> skip entirely. No additionalContext key, no noise.
|
|
91
|
+
if [ -z "$block" ]; then
|
|
92
|
+
if command -v bus_emit >/dev/null 2>&1; then
|
|
93
|
+
payload="$(jq -cn '{hook:"task-skill-route", decision:"ok",
|
|
94
|
+
reason:"no skills matched (empty buckets)"}' 2>/dev/null)"
|
|
95
|
+
bus_emit "$session" "hook_fire" "$tool" "completed" "$payload"
|
|
96
|
+
fi
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Build the PreToolUse hookSpecificOutput envelope. additionalContext is
|
|
101
|
+
# the documented field for advisory text prepended to the tool's prompt
|
|
102
|
+
# (Claude Code hooks docs, PreToolUse section). We do NOT set
|
|
103
|
+
# permissionDecision — advisory only, never a gate.
|
|
104
|
+
jq -cn --arg ctx "$block" '{
|
|
105
|
+
hookSpecificOutput: {
|
|
106
|
+
hookEventName: "PreToolUse",
|
|
107
|
+
additionalContext: $ctx
|
|
108
|
+
}
|
|
109
|
+
}'
|
|
110
|
+
|
|
111
|
+
if command -v bus_emit >/dev/null 2>&1; then
|
|
112
|
+
# Truncate the bus payload — the full block lives in the Task's context,
|
|
113
|
+
# not in the event log.
|
|
114
|
+
preview="$(printf '%s' "$block" | head -c 200 | tr '\n' ' ')"
|
|
115
|
+
payload="$(jq -cn --arg p "$preview" \
|
|
116
|
+
'{hook:"task-skill-route", decision:"allow",
|
|
117
|
+
reason:"injected skill-discovery advisory",
|
|
118
|
+
preview:$p}' 2>/dev/null)"
|
|
119
|
+
bus_emit "$session" "hook_fire" "$tool" "completed" "$payload"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
exit 0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# MISHKAN SessionStart — skill-index drift check (D-011 Phase 2).
|
|
3
|
+
#
|
|
4
|
+
# Runs the universal skill-discovery indexer in --stat-only mode at the start
|
|
5
|
+
# of every session. The indexer compares each known SKILL.md mtime against
|
|
6
|
+
# meta.last_scan and rebuilds only on drift (or if the index is missing).
|
|
7
|
+
#
|
|
8
|
+
# Performance contract: <= 200 ms p95. The stat-only sweep is just mtime calls
|
|
9
|
+
# against ~200 files; a full rebuild (rare) is bounded by file count and runs
|
|
10
|
+
# in well under a second.
|
|
11
|
+
#
|
|
12
|
+
# Fail-open by contract: any error -> exit 0 silently. The discovery layer
|
|
13
|
+
# never blocks a session boot. If indexing fails, the router will surface
|
|
14
|
+
# `index_missing_or_unreadable` on its next call and /mishkan-skills-reindex
|
|
15
|
+
# is the recovery path.
|
|
16
|
+
#
|
|
17
|
+
# Wired in payload/install/settings.hooks.json under SessionStart.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
MISHKAN_HOME_RES="${MISHKAN_HOME:-$HOME/.claude/mishkan}"
|
|
22
|
+
INDEXER="${MISHKAN_HOME_RES}/scripts/skill-discovery-indexer.py"
|
|
23
|
+
|
|
24
|
+
# Indexer missing (older harness install / partial payload) -> noop.
|
|
25
|
+
[ -f "$INDEXER" ] || exit 0
|
|
26
|
+
|
|
27
|
+
# python3 absent -> noop. Never block session boot on a missing interpreter.
|
|
28
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
29
|
+
|
|
30
|
+
# stat-only does the cheap path; rebuilds only on drift. --quiet keeps
|
|
31
|
+
# stdout empty so SessionStart additionalContext isn't polluted by indexer
|
|
32
|
+
# noise (this hook is purely a side-effect; the router consumes the index).
|
|
33
|
+
python3 "$INDEXER" --stat-only --quiet >/dev/null 2>&1 || true
|
|
34
|
+
|
|
35
|
+
exit 0
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"type": "object",
|
|
86
86
|
"required": ["hook"],
|
|
87
87
|
"properties": {
|
|
88
|
-
"hook": {"type": "string", "description": "hook name: ira, model-route,
|
|
88
|
+
"hook": {"type": "string", "description": "hook name: ira, model-route, knowledge-route, trace, observe"},
|
|
89
89
|
"decision": {"type": "string", "enum": ["allow", "deny", "advise", "ok", "blocked"]},
|
|
90
90
|
"reason": {"type": "string"}
|
|
91
91
|
}
|