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.
Files changed (113) hide show
  1. package/README.md +108 -172
  2. package/bin/mishkan.js +677 -16
  3. package/docs/design/MISHKAN_decisions.md +450 -8
  4. package/docs/design/MISHKAN_harness_design.md +1 -33
  5. package/docs/design/MISHKAN_observability.md +13 -9
  6. package/docs/research/graphify-token-saving-poc.md +108 -0
  7. package/docs/usage/09-workflows.md +74 -4
  8. package/docs/usage/10-observability.md +37 -13
  9. package/docs/usage/11-graphify.md +264 -0
  10. package/docs/usage/12-skill-discovery.md +294 -0
  11. package/docs/usage/README.md +5 -3
  12. package/package.json +1 -1
  13. package/payload/install/settings.hooks.json +24 -0
  14. package/payload/mishkan/AGENT_SPEC.md +25 -7
  15. package/payload/mishkan/agents/ahikam.md +4 -2
  16. package/payload/mishkan/agents/aholiab.md +4 -2
  17. package/payload/mishkan/agents/asaph.md +5 -2
  18. package/payload/mishkan/agents/baruch.md +19 -2
  19. package/payload/mishkan/agents/benaiah.md +4 -2
  20. package/payload/mishkan/agents/bezalel.md +4 -2
  21. package/payload/mishkan/agents/caleb.md +4 -2
  22. package/payload/mishkan/agents/deborah.md +4 -2
  23. package/payload/mishkan/agents/elasah.md +4 -2
  24. package/payload/mishkan/agents/eliashib.md +4 -2
  25. package/payload/mishkan/agents/ezra.md +4 -2
  26. package/payload/mishkan/agents/hanun.md +5 -2
  27. package/payload/mishkan/agents/hiram.md +5 -2
  28. package/payload/mishkan/agents/hizkiah.md +5 -2
  29. package/payload/mishkan/agents/huldah.md +4 -2
  30. package/payload/mishkan/agents/huram.md +4 -2
  31. package/payload/mishkan/agents/hushai.md +5 -2
  32. package/payload/mishkan/agents/igal.md +4 -2
  33. package/payload/mishkan/agents/ira.md +5 -2
  34. package/payload/mishkan/agents/jahaziel.md +5 -2
  35. package/payload/mishkan/agents/jakin.md +4 -2
  36. package/payload/mishkan/agents/jehonathan.md +5 -2
  37. package/payload/mishkan/agents/jehoshaphat.md +4 -2
  38. package/payload/mishkan/agents/joab.md +5 -2
  39. package/payload/mishkan/agents/joah.md +5 -2
  40. package/payload/mishkan/agents/maaseiah.md +4 -2
  41. package/payload/mishkan/agents/meremoth.md +5 -2
  42. package/payload/mishkan/agents/meshullam.md +5 -2
  43. package/payload/mishkan/agents/nathan.md +5 -2
  44. package/payload/mishkan/agents/nehemiah.md +4 -2
  45. package/payload/mishkan/agents/obed.md +4 -2
  46. package/payload/mishkan/agents/oholiab.md +5 -2
  47. package/payload/mishkan/agents/palal.md +5 -2
  48. package/payload/mishkan/agents/phinehas.md +4 -2
  49. package/payload/mishkan/agents/rehum.md +4 -2
  50. package/payload/mishkan/agents/salma.md +5 -2
  51. package/payload/mishkan/agents/seraiah.md +4 -2
  52. package/payload/mishkan/agents/shallum.md +5 -2
  53. package/payload/mishkan/agents/shaphan.md +4 -2
  54. package/payload/mishkan/agents/shemaiah.md +4 -2
  55. package/payload/mishkan/agents/shevna.md +5 -2
  56. package/payload/mishkan/agents/uriah.md +5 -2
  57. package/payload/mishkan/agents/zaccur.md +4 -2
  58. package/payload/mishkan/agents/zadok.md +5 -2
  59. package/payload/mishkan/agents/zerubbabel.md +4 -2
  60. package/payload/mishkan/commands/code-graph.md +35 -0
  61. package/payload/mishkan/commands/eval-baruch.md +57 -0
  62. package/payload/mishkan/commands/mishkan-org-reference.md +58 -0
  63. package/payload/mishkan/commands/mishkan-skills-misses.md +47 -0
  64. package/payload/mishkan/commands/mishkan-skills-reindex.md +33 -0
  65. package/payload/mishkan/commands/skills.md +35 -0
  66. package/payload/mishkan/commands/sprint-close.md +3 -1
  67. package/payload/mishkan/hooks/pre-tool-knowledge-route.sh +231 -0
  68. package/payload/mishkan/hooks/pre-tool-task-skill-route.sh +122 -0
  69. package/payload/mishkan/hooks/session-start-skill-index.sh +35 -0
  70. package/payload/mishkan/observability/schema.json +1 -1
  71. package/payload/mishkan/observability/watch/src/mishkan_watch/__main__.py +84 -2
  72. package/payload/mishkan/observability/watch/src/mishkan_watch/app.py +64 -3
  73. package/payload/mishkan/observability/watch/src/mishkan_watch/org_data.py +79 -0
  74. package/payload/mishkan/observability/watch/src/mishkan_watch/skills_data.py +267 -0
  75. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/agents.py +33 -1
  76. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/knowledge.py +118 -9
  77. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/live.py +102 -2
  78. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/org.py +139 -0
  79. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/skills.py +187 -0
  80. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/usage.py +377 -0
  81. package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/workflows.py +199 -27
  82. package/payload/mishkan/observability/watch/src/mishkan_watch/theme.tcss +78 -4
  83. package/payload/mishkan/observability/watchd/src/mishkan_watchd/__main__.py +36 -6
  84. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/bus_tail.py +16 -3
  85. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/graphify_tail.py +220 -0
  86. package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/worktree_poll.py +13 -2
  87. package/payload/mishkan/observability/watchd/src/mishkan_watchd/state.py +157 -3
  88. package/payload/mishkan/observability/watchd/tests/test_state.py +6 -2
  89. package/payload/mishkan/org/org.json +141 -0
  90. package/payload/mishkan/scripts/pre-commit-eval-gate.sh +50 -0
  91. package/payload/mishkan/scripts/skill-discovery-indexer.py +462 -0
  92. package/payload/mishkan/scripts/skill-discovery-misses.py +173 -0
  93. package/payload/mishkan/scripts/skill-discovery-router.py +457 -0
  94. package/payload/mishkan/scripts/skill-discovery-smoke.sh +156 -0
  95. package/payload/mishkan/skills/graphify-query-craft/SKILL.md +162 -0
  96. package/payload/mishkan/skills/mishkan-init/SKILL.md +15 -0
  97. package/payload/mishkan/skills/skill-discovery/SKILL.md +120 -0
  98. package/payload/mishkan/templates/case-node.schema.json +1 -0
  99. package/payload/mishkan/templates/observability-log.schema.json +1 -0
  100. package/payload/mishkan/workflows/README.md +78 -13
  101. package/payload/mishkan/workflows/chosheb-feature-ship.js +75 -0
  102. package/payload/mishkan/workflows/migdal-dr-drill.js +94 -0
  103. package/payload/mishkan/workflows/migdal-infra-change.js +67 -0
  104. package/payload/mishkan/workflows/mishkan-blast-radius.js +144 -0
  105. package/payload/mishkan/workflows/mishkan-init.js +58 -0
  106. package/payload/mishkan/workflows/mishkan-knowledge-gap-discovery.js +121 -0
  107. package/payload/mishkan/workflows/mishkan-standards-rollout.js +153 -0
  108. package/payload/mishkan/workflows/mishmar-security-gate.js +83 -0
  109. package/payload/mishkan/workflows/panim-ds-rollout.js +83 -0
  110. package/payload/mishkan/workflows/sefer-release-notes.js +85 -0
  111. package/payload/mishkan/workflows/yasad-data-migration-wave.js +78 -0
  112. package/payload/mishkan/workflows/yasad-schema-evolution.js +76 -0
  113. 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; improvement-layer queries refresh.
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, graphify-nudge, trace, observe"},
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
  }