@meetless/mla 0.1.4
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/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# post-tool-use.sh: Claude Code PostToolUse hook (Bash + meetless MCP).
|
|
3
|
+
#
|
|
4
|
+
# Two routes, selected by tool name:
|
|
5
|
+
# Bash -> tool_used_bash event (command, exit code,
|
|
6
|
+
# stdout/stderr tails, category HINT) spooled to
|
|
7
|
+
# the queue; Worker re-categorizes (Smaller-D).
|
|
8
|
+
# mcp__meetless__meetless__* -> tool_used_mcp record of the agent's OWN
|
|
9
|
+
# evidence pull, written LOCALLY to
|
|
10
|
+
# logs/mcp-calls.jsonl keyed by (session_id,
|
|
11
|
+
# turn_index). This is the "pull" side of A1
|
|
12
|
+
# evidence-followthrough: ask-traces.jsonl says
|
|
13
|
+
# what we injected on a turn, mcp-calls.jsonl
|
|
14
|
+
# says what the agent pulled on the same turn.
|
|
15
|
+
# relationship_verdict is an ACTION, never an
|
|
16
|
+
# evidence Pull (evidence_tool=false). See
|
|
17
|
+
# notes/20260603-mla-kb-agent-proxy-and-evidence-adoption.md
|
|
18
|
+
# §7.1 P1 / §7.4 A1.
|
|
19
|
+
# Any other tool is ignored.
|
|
20
|
+
#
|
|
21
|
+
# Source: notes/20260527-bare-bones-mvp-codebase-evaluation-and-plan.md §5.2.
|
|
22
|
+
source "$(dirname "$0")/common.sh"
|
|
23
|
+
|
|
24
|
+
# Per-folder activation gate (opt-in). Exit before any work unless a
|
|
25
|
+
# `.meetless.json` marker is found by walking up from $PWD. See
|
|
26
|
+
# meetless_activated in common.sh. Run `mla activate` in a repo to opt in.
|
|
27
|
+
meetless_activated || exit 0
|
|
28
|
+
|
|
29
|
+
INPUT="$(cat)"
|
|
30
|
+
# Wedge v6 Epoch 29: validate stdin parses as JSON BEFORE any jq substitution.
|
|
31
|
+
# See session-start.sh for the trap rationale.
|
|
32
|
+
if [[ -z "$INPUT" ]] || ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"
|
|
36
|
+
TOOL="$(echo "$INPUT" | jq -r '.tool_name // empty')"
|
|
37
|
+
[[ -z "$SESSION_ID" ]] && exit 0
|
|
38
|
+
# Per-session OFF override (`mla deactivate`). Silences this one session even in
|
|
39
|
+
# an activated folder. See meetless_session_disabled in common.sh.
|
|
40
|
+
meetless_session_disabled "$SESSION_ID" && exit 0
|
|
41
|
+
|
|
42
|
+
# F3-B liveness heartbeat. Throttled detached flush (<=1 per ~60s/session) drains
|
|
43
|
+
# the events already queued this turn so a long, tool-heavy turn keeps control's
|
|
44
|
+
# lastSeenAt fresh and stays LIVE instead of aging into IDLE mid-work. Spools no
|
|
45
|
+
# new event; fail-soft. Runs for EVERY tool (including non-spooling Read/Grep) so
|
|
46
|
+
# the heartbeat covers the whole turn, not just file/bash tools.
|
|
47
|
+
heartbeat_flush "$SESSION_ID"
|
|
48
|
+
|
|
49
|
+
# ---- Intra-turn narration capture (LIVE, per assistant entry) ------------
|
|
50
|
+
# The agent's visible prose between tool calls is the "line of thought" the
|
|
51
|
+
# session timeline needs INTERLEAVED with the commands. The Stop hook also
|
|
52
|
+
# captures narration, but only as ONE blob stamped at Stop-time -- so it lumps
|
|
53
|
+
# at the end of the turn (never interleaved) and reads the transcript only as it
|
|
54
|
+
# exists at Stop, AFTER a mid-turn auto-compaction has already destroyed the
|
|
55
|
+
# earlier prose. This hook fires LIVE after every tool, so it records each
|
|
56
|
+
# assistant text entry at its OWN transcript timestamp (correct interleave) and
|
|
57
|
+
# BEFORE a later compaction can drop it (compaction-robust). Dogfood-audit
|
|
58
|
+
# 2026-06-12: session f16d5e9a rendered as a wall of commands with no prose.
|
|
59
|
+
#
|
|
60
|
+
# Each entry is keyed by its transcript uuid (assistant_message:<uuid>) so a
|
|
61
|
+
# re-fired hook and the Stop backstop are idempotent against control's
|
|
62
|
+
# (runId, eventKey) dedup. A per-session ts cursor stops us re-spooling prose we
|
|
63
|
+
# already captured on the previous tool. The turn's CLOSING message
|
|
64
|
+
# (stop_reason end_turn) is EXCLUDED -- that is the Stop hook's finalMessage, not
|
|
65
|
+
# narration, and excluding it also stops a later turn from re-capturing the prior
|
|
66
|
+
# turn's closer. Only `text` content is taken (thinking blocks stay private).
|
|
67
|
+
# Runs for EVERY tool (incl. non-spooling Read/Grep) so prose on a read-only turn
|
|
68
|
+
# is not lost. Best-effort and fail-soft: a missing/unreadable transcript or any
|
|
69
|
+
# jq error skips capture and never disturbs the tool spool below. Narration is the
|
|
70
|
+
# default now (no kill switch): the timeline is wrong without it.
|
|
71
|
+
NARR_TRANSCRIPT="$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
|
|
72
|
+
if [[ -n "$NARR_TRANSCRIPT" && -f "$NARR_TRANSCRIPT" ]]; then
|
|
73
|
+
mkdir -p "$QUEUE_DIR"
|
|
74
|
+
NARR_CURSOR_FILE="$QUEUE_DIR/$SESSION_ID.narration-cursor"
|
|
75
|
+
(
|
|
76
|
+
flock 8
|
|
77
|
+
NARR_CURSOR="$(cat "$NARR_CURSOR_FILE" 2>/dev/null || echo '')"
|
|
78
|
+
# tail caps the per-fire read; the cursor guarantees completeness because
|
|
79
|
+
# each narration entry is followed immediately by the tool_use that fires
|
|
80
|
+
# this hook, so a new entry is always within the recent window.
|
|
81
|
+
NARR_LINES="$(tail -n 1200 "$NARR_TRANSCRIPT" 2>/dev/null | jq -c -R --slurp \
|
|
82
|
+
--arg sid "$SESSION_ID" --arg cursor "$NARR_CURSOR" '
|
|
83
|
+
split("\n")
|
|
84
|
+
| map(select(length > 0) | fromjson?)
|
|
85
|
+
| map(select(type == "object"))
|
|
86
|
+
| [ .[]
|
|
87
|
+
| select(.type == "assistant")
|
|
88
|
+
| select((.message.stop_reason // "") != "end_turn")
|
|
89
|
+
| { uuid: (.uuid // ""),
|
|
90
|
+
ts: (.timestamp // ""),
|
|
91
|
+
text: ( (.message.content // [])
|
|
92
|
+
| if type == "array"
|
|
93
|
+
then map(select((.type? // "") == "text") | (.text? // "")) | join("\n")
|
|
94
|
+
else "" end ) }
|
|
95
|
+
| select(.uuid != "" and .ts != "")
|
|
96
|
+
| select((.text | gsub("\\s"; "") | length) > 0)
|
|
97
|
+
| select(.ts > $cursor) ]
|
|
98
|
+
| .[]
|
|
99
|
+
| { ts: .ts,
|
|
100
|
+
event: "assistant_message",
|
|
101
|
+
eventKey: ("assistant_message:" + .uuid),
|
|
102
|
+
sessionId: $sid,
|
|
103
|
+
payload: { narration: .text, entryUuid: .uuid } }
|
|
104
|
+
' 2>/dev/null || true)"
|
|
105
|
+
if [[ -n "$NARR_LINES" ]]; then
|
|
106
|
+
while IFS= read -r NARR_LINE; do
|
|
107
|
+
[[ -z "$NARR_LINE" ]] && continue
|
|
108
|
+
spool_append "$SESSION_ID" "$NARR_LINE"
|
|
109
|
+
done <<< "$NARR_LINES"
|
|
110
|
+
NARR_NEW_CURSOR="$(printf '%s\n' "$NARR_LINES" | jq -rs 'map(.ts) | max // empty' 2>/dev/null || true)"
|
|
111
|
+
[[ -n "$NARR_NEW_CURSOR" ]] && printf '%s' "$NARR_NEW_CURSOR" > "$NARR_CURSOR_FILE"
|
|
112
|
+
fi
|
|
113
|
+
) 8>"$NARR_CURSOR_FILE.lock" || true
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# ---- meetless MCP-call capture (P1) -------------------------------------
|
|
117
|
+
# Record the agent's own evidence pulls before the Bash path. These land in a
|
|
118
|
+
# LOCAL file (not the Control spool) since A1 joins them against the local
|
|
119
|
+
# enrichment trace; keeping them out of the queue also leaves the Bash spool
|
|
120
|
+
# contract untouched.
|
|
121
|
+
if [[ "$TOOL" == mcp__meetless__meetless__* ]]; then
|
|
122
|
+
MCP_TOOL="${TOOL##*meetless__}"
|
|
123
|
+
# Evidence-bearing pulls vs actions. relationship_verdict mutates governance
|
|
124
|
+
# state; it is NOT a Pull for A1a. The three read tools return cited evidence.
|
|
125
|
+
case "$MCP_TOOL" in
|
|
126
|
+
retrieve_knowledge|kb_doc_detail|query) EVIDENCE_TOOL=true ;;
|
|
127
|
+
*) EVIDENCE_TOOL=false ;;
|
|
128
|
+
esac
|
|
129
|
+
QUERY="$(echo "$INPUT" | jq -r '.tool_input.query // .tool_input.question // .tool_input.citation // ""')"
|
|
130
|
+
# Scan both the call args and its result for citation tokens (the source_ids
|
|
131
|
+
# the agent actually touched). tojson keeps object/array/string shapes flat
|
|
132
|
+
# and preserves the literal [XX:id] tokens; extract_source_ids (common.sh) is
|
|
133
|
+
# the one shared grammar across the pull and push-reference sides.
|
|
134
|
+
SCAN="$(echo "$INPUT" | jq -r '{i: .tool_input, r: (.tool_response // .tool_result)} | tojson')"
|
|
135
|
+
SOURCE_IDS_JSON="$(extract_source_ids "$SCAN")"
|
|
136
|
+
# Attribute to the CURRENT turn (read, never advance: next_turn_index is owned
|
|
137
|
+
# by UserPromptSubmit). mkdir guards a tool call arriving before any prompt.
|
|
138
|
+
mkdir -p "$QUEUE_DIR" "$LOG_DIR"
|
|
139
|
+
TURN="$(current_turn_index "$SESSION_ID")"
|
|
140
|
+
TS="$(date -u +%FT%TZ)"
|
|
141
|
+
LINE="$(jq -c -n \
|
|
142
|
+
--arg ts "$TS" --arg event "tool_used_mcp" \
|
|
143
|
+
--arg sessionId "$SESSION_ID" --argjson turn "$TURN" \
|
|
144
|
+
--arg tool "$MCP_TOOL" --argjson evidence "$EVIDENCE_TOOL" \
|
|
145
|
+
--arg query "$QUERY" --argjson sids "$SOURCE_IDS_JSON" \
|
|
146
|
+
'{ts: $ts, event: $event, session_id: $sessionId, turn_index: $turn, tool: $tool, evidence_tool: $evidence, query: $query, source_ids: $sids}')"
|
|
147
|
+
(
|
|
148
|
+
flock 9
|
|
149
|
+
printf '%s\n' "$LINE" >> "$LOG_DIR/mcp-calls.jsonl"
|
|
150
|
+
) 9>"$LOG_DIR/mcp-calls.lock"
|
|
151
|
+
|
|
152
|
+
# ---- InjectionTrace parity for the MCP surface (P0.2, design §7.6) --------
|
|
153
|
+
# MCP grounding is an INJECTION surface: an evidence-bearing pull returns cited
|
|
154
|
+
# relationships INTO this turn's context. The stateless MCP server has no session
|
|
155
|
+
# identity, but THIS hook does (SESSION_ID + the read-only TURN), so it is the
|
|
156
|
+
# one place that can emit an InjectionTrace-compatible record for the MCP path,
|
|
157
|
+
# reconciled to the run by riding its own session's event stream (the SAME spool
|
|
158
|
+
# + flush pipeline as the HOOK producer in user-prompt-submit.sh). Without it the
|
|
159
|
+
# session-detail "Injected" lane reads empty for an MCP-grounded run even though
|
|
160
|
+
# relationships were injected -- which is the exact dishonesty §7.6 makes P0.
|
|
161
|
+
#
|
|
162
|
+
# Lean §7.6 superset: contextItems are the citation tokens the grounding actually
|
|
163
|
+
# returned (no kind/status/confidence agentic enrichment), sourceSurface=MCP tells
|
|
164
|
+
# the console it is reading the lean shape. Emit ONLY on a REAL injection: an
|
|
165
|
+
# evidence tool that returned >=1 cited source (a pull with no citation injected
|
|
166
|
+
# nothing; relationship_verdict is an ACTION, EVIDENCE_TOOL=false, never an
|
|
167
|
+
# injection). deliveryStatus is stamped INJECTED HERE, by the surface, at the
|
|
168
|
+
# delivery moment (INV-INJECTIONTRACE-DELIVERY). The injectId IS the eventKey
|
|
169
|
+
# (minted once, replayed byte-identical on a re-flush so control's 6-tuple
|
|
170
|
+
# idempotency no-ops the retry); traceId reuses it since the MCP pull has no
|
|
171
|
+
# separate enrich trace id. Best-effort and fail-soft: a jq failure omits the
|
|
172
|
+
# record and never disturbs the local pull capture above.
|
|
173
|
+
if [[ "$EVIDENCE_TOOL" == "true" ]]; then
|
|
174
|
+
IT_ITEMS="$(printf '%s' "$SOURCE_IDS_JSON" | jq -c \
|
|
175
|
+
'[ (. // []) | unique | .[] | select(. != "") | {source_id: ., injected: true} ]' \
|
|
176
|
+
2>/dev/null || printf '[]')"
|
|
177
|
+
IT_COUNT="$(printf '%s' "$IT_ITEMS" | jq 'length' 2>/dev/null || printf 0)"
|
|
178
|
+
if [[ "${IT_COUNT:-0}" -gt 0 ]]; then
|
|
179
|
+
IT_KEY="$(gen_event_key)"
|
|
180
|
+
IT_LINE="$(jq -c -n \
|
|
181
|
+
--arg ts "$TS" --arg key "$IT_KEY" --arg session_id "$SESSION_ID" \
|
|
182
|
+
--argjson turn "${TURN:-0}" --argjson items "$IT_ITEMS" \
|
|
183
|
+
'{
|
|
184
|
+
ts: $ts, event: "injection_trace", eventKey: $key, sessionId: $session_id,
|
|
185
|
+
payload: {
|
|
186
|
+
sourceSurface: "MCP", turnIndex: $turn, injectId: $key, traceId: $key,
|
|
187
|
+
deliveryStatus: "INJECTED", schemaVersion: 1, status: null, confidence: null,
|
|
188
|
+
contextItems: $items, markdown: null, capturedAt: $ts
|
|
189
|
+
}
|
|
190
|
+
}' 2>/dev/null || true)"
|
|
191
|
+
[[ -n "$IT_LINE" ]] && spool_append "$SESSION_ID" "$IT_LINE"
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
exit 0
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# ---- AskUserQuestion agent-decision capture (provider-neutral) -----------
|
|
198
|
+
# Claude's AskUserQuestion bundles N questions in one tool call; each ANSWERED
|
|
199
|
+
# question becomes one first-class, auditable agent-human decision. Hand the raw
|
|
200
|
+
# hook payload (tool_input.questions + tool_response.answers + tool_use_id) to the
|
|
201
|
+
# `mla _internal capture-decisions` Claude normalizer, which emits one
|
|
202
|
+
# `agent_decision_captured` spool event per answered question (providerEventId =
|
|
203
|
+
# "<tool_use_id>#<questionIndex>"). The command is an IO-light PURE transform and
|
|
204
|
+
# touches NO spool itself; ALL spool locking stays HERE so the single-writer
|
|
205
|
+
# invariant lives in one place. Passing --spool lets the command dedup against
|
|
206
|
+
# eventKeys already queued this session (the Stop transcript-scan backstop writes
|
|
207
|
+
# the same keys), so a re-fired PostToolUse never double-spools the same decision.
|
|
208
|
+
# Capture is assistive: every failure is swallowed and never breaks the session.
|
|
209
|
+
# See notes/20260608-agent-decision-capture-design.md section 5.
|
|
210
|
+
if [[ "$TOOL" == "AskUserQuestion" ]]; then
|
|
211
|
+
if [[ -n "${MLA_PATH:-}" && -x "$MLA_PATH" ]]; then
|
|
212
|
+
DECISION_LINES="$(printf '%s' "$INPUT" | "$MLA_PATH" _internal capture-decisions \
|
|
213
|
+
--source post_tool_use --session "$SESSION_ID" \
|
|
214
|
+
--spool "$QUEUE_DIR/$SESSION_ID.jsonl" 2>/dev/null || true)"
|
|
215
|
+
while IFS= read -r DECISION_LINE; do
|
|
216
|
+
[[ -z "$DECISION_LINE" ]] && continue
|
|
217
|
+
spool_append "$SESSION_ID" "$DECISION_LINE"
|
|
218
|
+
done <<< "$DECISION_LINES"
|
|
219
|
+
fi
|
|
220
|
+
exit 0
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# ---- Governed trace: tool_used_file on file-modifying tools ---------------
|
|
224
|
+
# Dogfood-audit 2026-06-10 issue 3: tool capture was bash-only, so a code-only
|
|
225
|
+
# session (all Write/Edit, no Bash) left ZERO governed tool trace in control.
|
|
226
|
+
# Spool ONE metadata-only event per file-modifying call: {tool, filePath}. No
|
|
227
|
+
# file content, no diff, no tool I/O (the v0 privacy boundary stays intact; a
|
|
228
|
+
# path is milder evidence than the stdout/stderr tails the Bash spool ships).
|
|
229
|
+
#
|
|
230
|
+
# This is the PRIMARY governed artifact for a file-modifying turn, so it spools
|
|
231
|
+
# FIRST -- ahead of the assistive A2 produced-doc and DUR blocks below. Both of
|
|
232
|
+
# those can early-exit under `set -euo pipefail` (DUR exits 0 in every path; A2
|
|
233
|
+
# walks marker trees and can abort), and anything placed after such a block
|
|
234
|
+
# never executes. Capturing the governed fact before any best-effort enrichment
|
|
235
|
+
# is what keeps the trace immune to a downstream abort (the prose-outside-marker
|
|
236
|
+
# regression that the dogfood-audit follow-up locked).
|
|
237
|
+
if [[ "$TOOL" == "Edit" || "$TOOL" == "Write" || "$TOOL" == "MultiEdit" || "$TOOL" == "NotebookEdit" ]]; then
|
|
238
|
+
FILE_TRACE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty')"
|
|
239
|
+
if [[ -n "$FILE_TRACE_PATH" ]]; then
|
|
240
|
+
TS="$(date -u +%FT%TZ)"
|
|
241
|
+
EVENT_KEY="$(gen_event_key)"
|
|
242
|
+
LINE="$(jq -c -n \
|
|
243
|
+
--arg ts "$TS" --arg event "tool_used_file" --arg key "$EVENT_KEY" \
|
|
244
|
+
--arg sessionId "$SESSION_ID" --arg tool "$TOOL" --arg fp "$FILE_TRACE_PATH" \
|
|
245
|
+
'{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {tool: $tool, filePath: $fp}}')"
|
|
246
|
+
spool_append "$SESSION_ID" "$LINE"
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# ---- Read-side knowledge trace: tool_used_file with access:"read" ---------
|
|
251
|
+
# Session Files rail Phase 2 (notes/20260616-session-files-rail-design.md). The
|
|
252
|
+
# rail's "read by the agent" lane needs to know which PROSE files the agent
|
|
253
|
+
# opened; Read/Grep/Glob otherwise emit nothing. Spool ONE metadata-only
|
|
254
|
+
# tool_used_file per markdown Read, tagged access:"read" so the console routes it
|
|
255
|
+
# to the read lane (not produced) and the timeline labels it "Read a file" rather
|
|
256
|
+
# than "Edited a file". Gate STRICTLY to prose_path_allowed (the same allowlist
|
|
257
|
+
# the input/produced lanes use) so a code Read spools nothing and the stream is
|
|
258
|
+
# not flooded with every source-file open. Metadata only ({tool, filePath,
|
|
259
|
+
# access}), never file content. exit 0 keeps this read trace self-contained: the
|
|
260
|
+
# assistive A2 / DUR / Bash blocks below only handle modifying tools and Bash.
|
|
261
|
+
if [[ "$TOOL" == "Read" ]]; then
|
|
262
|
+
READ_TRACE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')"
|
|
263
|
+
if [[ -n "$READ_TRACE_PATH" ]] && prose_path_allowed "$READ_TRACE_PATH"; then
|
|
264
|
+
TS="$(date -u +%FT%TZ)"
|
|
265
|
+
EVENT_KEY="$(gen_event_key)"
|
|
266
|
+
LINE="$(jq -c -n \
|
|
267
|
+
--arg ts "$TS" --arg event "tool_used_file" --arg key "$EVENT_KEY" \
|
|
268
|
+
--arg sessionId "$SESSION_ID" --arg tool "$TOOL" --arg fp "$READ_TRACE_PATH" \
|
|
269
|
+
'{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {tool: $tool, filePath: $fp, access: "read"}}')"
|
|
270
|
+
spool_append "$SESSION_ID" "$LINE"
|
|
271
|
+
fi
|
|
272
|
+
exit 0
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# ---- Route 4: A2 produced/updated-doc capture (Zone 1, Phase 0) ----------
|
|
276
|
+
# Mark prose docs the agent wrote/edited this turn into the Active Review store.
|
|
277
|
+
# Pure local append: no detector, no KB write, no network. The envelope carries
|
|
278
|
+
# the real ownerUserId/workspaceId (never placeholders) so Phases 3-5 never
|
|
279
|
+
# migrate. Silence by default. Spec tests 1,2,7,24,40,41.
|
|
280
|
+
#
|
|
281
|
+
# ASSISTIVE + best-effort, so the whole block runs in a subshell guarded by
|
|
282
|
+
# `|| true`. NOTHING inside can abort the parent hook under `set -euo pipefail`:
|
|
283
|
+
# not meetless_repo_root returning 1 for a file outside every marker, not a
|
|
284
|
+
# content_hash shasum failure on an unreadable file (non-zero under pipefail),
|
|
285
|
+
# not a failed record_active_memory write, not any future addition. The primary
|
|
286
|
+
# tool_used_file trace already spooled ABOVE, so a hard abort here costs at most
|
|
287
|
+
# this turn's prose enrichment, never the governed trace and never a non-zero
|
|
288
|
+
# PostToolUse exit. Vars set here intentionally do not leak; no later block reads
|
|
289
|
+
# A2_*.
|
|
290
|
+
(
|
|
291
|
+
case "$TOOL" in
|
|
292
|
+
Write|Edit|MultiEdit|NotebookEdit)
|
|
293
|
+
A2_FILE="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty')"
|
|
294
|
+
if [[ -n "$A2_FILE" ]] && prose_path_allowed "$A2_FILE"; then
|
|
295
|
+
A2_ROOT="$(meetless_repo_root "$(dirname "$A2_FILE")")"
|
|
296
|
+
if [[ -n "$A2_ROOT" && -f "$A2_FILE" ]]; then
|
|
297
|
+
# T1.2 cutover: the produced doc belongs to the workspace of the folder
|
|
298
|
+
# it LIVES in (nearest-wins for the edited file), not the cli-config one
|
|
299
|
+
# and not necessarily the session's marker. A2_ROOT is that marker's dir.
|
|
300
|
+
A2_WS="$(jq -r '.workspaceId // empty' "$A2_ROOT/.meetless.json" 2>/dev/null)"
|
|
301
|
+
A2_OWNER="$(jq -r '.actorUserId // empty' "$CFG" 2>/dev/null)"
|
|
302
|
+
if [[ -n "$A2_WS" && -n "$A2_OWNER" ]]; then
|
|
303
|
+
A2_TURN="$(current_turn_index "$SESSION_ID")"
|
|
304
|
+
A2_RRH="$(repo_root_hash "$A2_ROOT")"
|
|
305
|
+
A2_CPATH="$(canonical_path "$A2_ROOT" "$A2_FILE")"
|
|
306
|
+
A2_CHASH="$(content_hash "$A2_FILE")"
|
|
307
|
+
record_active_memory "produced_doc" "$SESSION_ID" "$A2_TURN" "$A2_WS" "$A2_OWNER" "$A2_RRH" "$A2_CPATH" "$A2_CHASH" "$A2_ROOT"
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
fi
|
|
311
|
+
;;
|
|
312
|
+
esac
|
|
313
|
+
) || true
|
|
314
|
+
|
|
315
|
+
# ---- DUR: just-in-time coordination flag on a governed-surface edit (§5.4 DURING)
|
|
316
|
+
# When the agent edits/writes a file, raise an ADVISORY flag iff a high-confidence
|
|
317
|
+
# coordination trigger from THIS turn names the surface being touched ("this
|
|
318
|
+
# surface is governed by X") at the moment of the edit, not a judgment of the edit
|
|
319
|
+
# itself. Reuses the BEFORE-turn imperative's rung-2 contract: turn-keyed state +
|
|
320
|
+
# the closed CoordinationTrigger enum + the P5 high-confidence floor. It NEVER
|
|
321
|
+
# blocks (P6 "never its hands"): it emits hookSpecificOutput.additionalContext,
|
|
322
|
+
# never `decision: "block"`. Dormant by default in prod (detectors are the producer
|
|
323
|
+
# of coordination_triggers and are mostly unwired, so no state file is written and
|
|
324
|
+
# this no-ops). See notes/20260603-mla-kb-agent-proxy-and-evidence-adoption.md §5.4
|
|
325
|
+
# / §6 #9 / §7.2 row "DUR".
|
|
326
|
+
if [[ "$TOOL" == "Edit" || "$TOOL" == "Write" || "$TOOL" == "MultiEdit" || "$TOOL" == "NotebookEdit" ]]; then
|
|
327
|
+
# Kill switch (default on; set MEETLESS_COORDINATION_DURING=0 to silence).
|
|
328
|
+
[[ "${MEETLESS_COORDINATION_DURING:-1}" == "0" ]] && exit 0
|
|
329
|
+
|
|
330
|
+
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty')"
|
|
331
|
+
[[ -z "$FILE_PATH" ]] && exit 0
|
|
332
|
+
|
|
333
|
+
STATE_FILE="$(coordination_state_file "$SESSION_ID")"
|
|
334
|
+
[[ -f "$STATE_FILE" ]] || exit 0
|
|
335
|
+
|
|
336
|
+
# Turn-match: stale state from a prior turn must NOT fire. current_turn_index
|
|
337
|
+
# peeks the turn UserPromptSubmit set; PostToolUse never advances it, so a
|
|
338
|
+
# mid-turn edit shares the enriched turn's index and an older file fails here.
|
|
339
|
+
STATE_TURN="$(jq -r '.turn_index // empty' "$STATE_FILE" 2>/dev/null || true)"
|
|
340
|
+
CUR_TURN="$(current_turn_index "$SESSION_ID" 2>/dev/null || printf 0)"
|
|
341
|
+
[[ -n "$STATE_TURN" && "$STATE_TURN" == "$CUR_TURN" ]] || exit 0
|
|
342
|
+
|
|
343
|
+
# P5 high-confidence floor (the same boundary the BEFORE-turn imperative holds;
|
|
344
|
+
# a trigger on a low/medium-confidence turn stays passive).
|
|
345
|
+
STATE_CONF="$(jq -r '.confidence // empty' "$STATE_FILE" 2>/dev/null || true)"
|
|
346
|
+
[[ "$STATE_CONF" == "high" ]] || exit 0
|
|
347
|
+
|
|
348
|
+
# Match the edited surface against this turn's triggers, hard-filtered to the
|
|
349
|
+
# closed enum (a malformed or injected type can never fire). The trigger surface
|
|
350
|
+
# is repo-relative; suffix-match it against the absolute edited path.
|
|
351
|
+
STATE_TRIGGERS="$(jq -c '.triggers // []' "$STATE_FILE" 2>/dev/null || printf '[]')"
|
|
352
|
+
MATCHED="$(printf '%s' "$STATE_TRIGGERS" | jq -c \
|
|
353
|
+
--arg fp "$FILE_PATH" --argjson enum "$COORDINATION_TRIGGER_ENUM" '
|
|
354
|
+
map(select(.type as $t | $enum | index($t)))
|
|
355
|
+
| map(select(.surface as $s
|
|
356
|
+
| ($s != null and $s != "")
|
|
357
|
+
and (($fp == $s) or ($fp | endswith("/" + $s)))))
|
|
358
|
+
' 2>/dev/null || printf '[]')"
|
|
359
|
+
MATCH_COUNT="$(printf '%s' "$MATCHED" | jq 'length' 2>/dev/null || printf 0)"
|
|
360
|
+
[[ "${MATCH_COUNT:-0}" -gt 0 ]] || exit 0
|
|
361
|
+
|
|
362
|
+
# No spam: flag a given surface at most once per session.
|
|
363
|
+
mkdir -p "$(coordination_dir)" 2>/dev/null || true
|
|
364
|
+
FLAGGED_FILE="$(coordination_flagged_file "$SESSION_ID")"
|
|
365
|
+
if [[ -f "$FLAGGED_FILE" ]] && grep -qxF "$FILE_PATH" "$FLAGGED_FILE" 2>/dev/null; then
|
|
366
|
+
exit 0
|
|
367
|
+
fi
|
|
368
|
+
(
|
|
369
|
+
flock 9
|
|
370
|
+
printf '%s\n' "$FILE_PATH" >> "$FLAGGED_FILE"
|
|
371
|
+
) 9>"$FLAGGED_FILE.lock"
|
|
372
|
+
|
|
373
|
+
STATE_TRACE="$(jq -r '.trace_id // ""' "$STATE_FILE" 2>/dev/null || true)"
|
|
374
|
+
COORD_LINES="$(printf '%s' "$MATCHED" | jq -r '.[] |
|
|
375
|
+
" - " + .type + (if (.ref // "") != "" then " -> " + .ref else "" end)' 2>/dev/null || true)"
|
|
376
|
+
CTX="<meetless-context kind=\"coordination\" surface=\"$FILE_PATH\" trace=\"$STATE_TRACE\">
|
|
377
|
+
You just edited a governed surface (just-in-time coordination flag): $FILE_PATH
|
|
378
|
+
Coordination applies before you rely on this change:
|
|
379
|
+
$COORD_LINES
|
|
380
|
+
This is a Meetless governance directive (computed server-side, not retrieved text). It is a reminder, not a block: Meetless never stops your tools. Pull the cited decision with meetless__kb_doc_detail and confirm the accountable owner signed off before you rely on this change.
|
|
381
|
+
</meetless-context>"
|
|
382
|
+
jq -n --arg ctx "$CTX" \
|
|
383
|
+
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$ctx}}'
|
|
384
|
+
exit 0
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
[[ "$TOOL" != "Bash" ]] && exit 0
|
|
388
|
+
|
|
389
|
+
CMD="$(echo "$INPUT" | jq -r '.tool_input.command // ""')"
|
|
390
|
+
EXIT_CODE="$(echo "$INPUT" | jq -r '.tool_result.exit_code // .tool_response.exit_code // empty')"
|
|
391
|
+
# Smaller-C: normalize empty/non-numeric to 0 BEFORE --argjson (jq dies on non-numeric).
|
|
392
|
+
if ! [[ "${EXIT_CODE:-}" =~ ^[0-9]+$ ]]; then EXIT_CODE=0; fi
|
|
393
|
+
# Truncate inside jq (codepoint-aware) so multibyte UTF-8 in tool output (e.g.
|
|
394
|
+
# Vietnamese console.log strings, emoji, accented test names) is never split
|
|
395
|
+
# mid-sequence. `tail -c 2000` cuts on bytes, which on a 3-byte vi character
|
|
396
|
+
# produces invalid UTF-8 that downstream jq --arg + Prisma JSON storage may
|
|
397
|
+
# silently corrupt or reject.
|
|
398
|
+
STDOUT_TAIL="$(echo "$INPUT" | jq -r '(.tool_result.stdout // .tool_response.stdout // "")[-2000:]')"
|
|
399
|
+
STDERR_TAIL="$(echo "$INPUT" | jq -r '(.tool_result.stderr // .tool_response.stderr // "")[-2000:]')"
|
|
400
|
+
TS="$(date -u +%FT%TZ)"
|
|
401
|
+
EVENT_KEY="$(gen_event_key)"
|
|
402
|
+
|
|
403
|
+
# Smaller-D: HINT only. The WORKER re-categorizes authoritatively from CMD.
|
|
404
|
+
CATEGORY_HINT="$(printf '%s' "$CMD" | awk '
|
|
405
|
+
/pytest|jest|vitest|mocha|go test|cargo test|pnpm test|npm test|yarn test/ {print "test"; exit}
|
|
406
|
+
/tsc|mypy|pyright/ {print "typecheck"; exit}
|
|
407
|
+
/eslint|ruff|flake8|prettier --check/ {print "lint"; exit}
|
|
408
|
+
/build|webpack|vite build|tsc -b|next build/ {print "build"; exit}
|
|
409
|
+
/prisma migrate|alembic|knex migrate/ {print "migration"; exit}
|
|
410
|
+
/npm i|pnpm i|pnpm add|yarn add|pip install|poetry add/ {print "package_install"; exit}
|
|
411
|
+
/^git / {print "git"; exit}
|
|
412
|
+
{print "unknown_bash"}
|
|
413
|
+
')"
|
|
414
|
+
|
|
415
|
+
LINE="$(jq -c -n \
|
|
416
|
+
--arg ts "$TS" --arg event "tool_used_bash" --arg key "$EVENT_KEY" \
|
|
417
|
+
--arg sessionId "$SESSION_ID" --arg cmd "$CMD" --arg hint "$CATEGORY_HINT" \
|
|
418
|
+
--arg out "$STDOUT_TAIL" --arg err "$STDERR_TAIL" --argjson exit "$EXIT_CODE" \
|
|
419
|
+
'{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {categoryHint: $hint, command: $cmd, exitCode: $exit, stdoutTail: $out, stderrTail: $err}}')"
|
|
420
|
+
|
|
421
|
+
spool_append "$SESSION_ID" "$LINE"
|
|
422
|
+
|
|
423
|
+
exit 0
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pre-tool-use.sh: Claude Code PreToolUse hook (R1 notes-location enforcement, A1).
|
|
3
|
+
#
|
|
4
|
+
# Scope (registered by wire.ts MANAGED_HOOK_SCRIPTS with matcher "^(Write|Edit)$"):
|
|
5
|
+
# fires only before a Write or Edit tool call. It hands the raw PreToolUse stdin to
|
|
6
|
+
# `mla _internal pretool-observe`, which runs the version-backed enforce seam and
|
|
7
|
+
# prints the hook response: either the empty `{}` pass-through, or a real deny body
|
|
8
|
+
# when a human-attested LIVE rule version is VIOLATED and the deny is admitted.
|
|
9
|
+
#
|
|
10
|
+
# This wrapper FORWARDS that response verbatim. The decision is computed by the
|
|
11
|
+
# subcommand (against a human-attested version), never by this script and never
|
|
12
|
+
# reflected from input. The subcommand always exits 0 and prints exactly one JSON
|
|
13
|
+
# body, so a non-empty stdout is a real, computed decision and is safe to relay.
|
|
14
|
+
#
|
|
15
|
+
# Fail open, always. No `set -e`: every step is best-effort. If `mla` is missing,
|
|
16
|
+
# crashes, hangs past the timeout, or prints nothing, this wrapper emits the empty
|
|
17
|
+
# `{}` pass-through and exits 0. A non-zero exit (especially 2) would BLOCK the tool,
|
|
18
|
+
# so this script NEVER exits non-zero: the decision rides the body, never the code.
|
|
19
|
+
|
|
20
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
21
|
+
|
|
22
|
+
# Resolve the absolute mla path the same way common.sh does (install-time path in
|
|
23
|
+
# cli-config.json, then PATH fallback). MLA in PATH is not relied upon.
|
|
24
|
+
CFG="${MEETLESS_HOME:-$HOME/.meetless}/cli-config.json"
|
|
25
|
+
MLA_PATH="$(jq -r '.mlaPath // empty' "$CFG" 2>/dev/null || true)"
|
|
26
|
+
if [[ -z "${MLA_PATH:-}" || ! -x "$MLA_PATH" ]]; then
|
|
27
|
+
MLA_PATH="$(command -v mla 2>/dev/null || true)"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Run a command under a wall-clock guard so a slow or stuck evaluation degrades to
|
|
31
|
+
# pass-through rather than hanging the tool. GNU `timeout` (or `gtimeout` from
|
|
32
|
+
# coreutils on macOS) is used when present; otherwise the command runs unguarded.
|
|
33
|
+
run_guarded() {
|
|
34
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
35
|
+
timeout 5 "$@"
|
|
36
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
37
|
+
gtimeout 5 "$@"
|
|
38
|
+
else
|
|
39
|
+
"$@"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Prefer the minimal sibling entrypoint (`pretool-entry.js`, emitted next to the
|
|
44
|
+
# resolved mla binary) when present: it pays only the deny-decision require graph
|
|
45
|
+
# (~12ms cold) instead of cli.js's full command registry (~150ms), the latency
|
|
46
|
+
# lever from notes/20260615-...-consolidated-proposal.md. Both transports call the
|
|
47
|
+
# identical runInternalPretoolObserve core, so the decision body is byte-identical.
|
|
48
|
+
# When the sibling is absent (a pkg binary, an older install), fall back to
|
|
49
|
+
# `mla _internal pretool-observe` so the slow path stays correct. It is run the same
|
|
50
|
+
# way as mla (its `#!/usr/bin/env node` shebang resolves node), under the same guard.
|
|
51
|
+
RESPONSE=""
|
|
52
|
+
if [[ -n "${MLA_PATH:-}" && -x "$MLA_PATH" ]]; then
|
|
53
|
+
PRETOOL_ENTRY="$(dirname "$MLA_PATH")/pretool-entry.js"
|
|
54
|
+
if [[ -x "$PRETOOL_ENTRY" ]]; then
|
|
55
|
+
RESPONSE="$(printf '%s' "$INPUT" | run_guarded "$PRETOOL_ENTRY" 2>/dev/null || true)"
|
|
56
|
+
else
|
|
57
|
+
RESPONSE="$(printf '%s' "$INPUT" | run_guarded "$MLA_PATH" _internal pretool-observe 2>/dev/null || true)"
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Forward the computed decision body if there is one; otherwise fall open to the
|
|
62
|
+
# empty no-decision body. Stripping whitespace guards against a stray newline-only
|
|
63
|
+
# stdout being mistaken for a real response.
|
|
64
|
+
if [[ -n "${RESPONSE//[[:space:]]/}" ]]; then
|
|
65
|
+
printf '%s' "$RESPONSE"
|
|
66
|
+
else
|
|
67
|
+
printf '{}'
|
|
68
|
+
fi
|
|
69
|
+
exit 0
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-start.sh: Claude Code SessionStart hook.
|
|
3
|
+
# Writes a session_started event to the spool and spawns a detached flush.
|
|
4
|
+
#
|
|
5
|
+
# Source: notes/20260527-bare-bones-mvp-codebase-evaluation-and-plan.md §5.2.
|
|
6
|
+
source "$(dirname "$0")/common.sh"
|
|
7
|
+
|
|
8
|
+
# Per-folder activation gate (opt-in). In an ACTIVATED repo we fall through to
|
|
9
|
+
# capture. In an UNACTIVATED repo we no longer exit silently: hand off to the CLI
|
|
10
|
+
# (which reuses the SAME marker resolver as `mla mcp`) to surface a one-line
|
|
11
|
+
# SessionStart explanation when warranted (logged-in git repos only); its stdout
|
|
12
|
+
# becomes Claude Code's additionalContext. No capture happens without a marker.
|
|
13
|
+
# See meetless_activated in common.sh. Run `mla activate` in a repo to opt in.
|
|
14
|
+
if ! meetless_activated; then
|
|
15
|
+
if [[ -n "${MLA_PATH:-}" && -x "$MLA_PATH" ]]; then
|
|
16
|
+
"$MLA_PATH" _internal session-nudge --cwd "$PWD" 2>/dev/null || true
|
|
17
|
+
fi
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Self-healing prune of dead hook entries in ~/.claude/settings.json.
|
|
22
|
+
# Background: hook entries can leak when temp worktrees (test fixtures, Claude
|
|
23
|
+
# Code worktrees, manual sandboxes) install themselves but skip cleanup on
|
|
24
|
+
# teardown. Without this, failed Stop / PostToolUse / etc. accumulate forever
|
|
25
|
+
# and Claude Code logs "Failed with non-blocking status code" on every event.
|
|
26
|
+
# Detection: walk every hook entry's `command` field; if it's a plain absolute
|
|
27
|
+
# path and does not exist on disk, drop that entry. Skips non-path commands
|
|
28
|
+
# (`pkill ...`, `~/.claude/...`, shell expressions). Whole block is best-effort
|
|
29
|
+
# and silenced so it can NEVER fail the hook.
|
|
30
|
+
{
|
|
31
|
+
__ml_settings="$HOME/.claude/settings.json"
|
|
32
|
+
if [[ -f "$__ml_settings" ]]; then
|
|
33
|
+
__ml_dead="$(jq -r '.hooks // {} | to_entries[] | .value[]? | .hooks[]?.command // empty' "$__ml_settings" 2>/dev/null \
|
|
34
|
+
| while IFS= read -r __cmd; do
|
|
35
|
+
[[ "$__cmd" =~ ^/[^[:space:]]+$ ]] && [[ ! -e "$__cmd" ]] && printf '%s\n' "$__cmd"
|
|
36
|
+
done)"
|
|
37
|
+
if [[ -n "$__ml_dead" ]]; then
|
|
38
|
+
__ml_dead_json="$(printf '%s\n' "$__ml_dead" | jq -R . | jq -s .)"
|
|
39
|
+
__ml_tmp="$__ml_settings.tmp.$$"
|
|
40
|
+
cp "$__ml_settings" "$__ml_settings.bak.meetless-prune-$(date +%Y%m%d-%H%M%S)" 2>/dev/null
|
|
41
|
+
if jq --argjson dead "$__ml_dead_json" '.hooks |= with_entries(.value |= map(select(([.hooks[]?.command] | map(tostring) | any(. as $c | $dead | index($c))) | not)))' "$__ml_settings" > "$__ml_tmp" 2>/dev/null \
|
|
42
|
+
&& jq empty "$__ml_tmp" 2>/dev/null; then
|
|
43
|
+
mv "$__ml_tmp" "$__ml_settings"
|
|
44
|
+
else
|
|
45
|
+
rm -f "$__ml_tmp" 2>/dev/null
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
} 2>/dev/null || true
|
|
50
|
+
|
|
51
|
+
INPUT="$(cat)"
|
|
52
|
+
# Wedge v6 Epoch 29: validate stdin parses as JSON BEFORE any jq substitution.
|
|
53
|
+
# Pre-fix the bare `SESSION_ID="$(echo "$INPUT" | jq -r ...)"` crashed under
|
|
54
|
+
# `set -euo pipefail` on empty stdin or malformed JSON: jq exits non-zero, the
|
|
55
|
+
# substitution propagates, the hook aborts BEFORE the empty-session-id guard
|
|
56
|
+
# below. Claude Code interprets that non-zero exit as a hook failure.
|
|
57
|
+
if [[ -z "$INPUT" ]] || ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"
|
|
61
|
+
[[ -z "$SESSION_ID" ]] && exit 0
|
|
62
|
+
# Per-session OFF override (`mla deactivate`). Silences this one session even in
|
|
63
|
+
# an activated folder. See meetless_session_disabled in common.sh.
|
|
64
|
+
meetless_session_disabled "$SESSION_ID" && exit 0
|
|
65
|
+
|
|
66
|
+
TRANSCRIPT="$(echo "$INPUT" | jq -r '.transcript_path // empty')"
|
|
67
|
+
CWD="$PWD"
|
|
68
|
+
BRANCH="$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
69
|
+
TS="$(date -u +%FT%TZ)"
|
|
70
|
+
EVENT_KEY="$(gen_event_key)"
|
|
71
|
+
|
|
72
|
+
# Best-effort current session name. A RESUMED session (`--resume` / `--continue`)
|
|
73
|
+
# starts with a transcript that already carries a title, so SessionStart is the
|
|
74
|
+
# earliest moment control can learn it (F3-A). Mirrors the local picker: human
|
|
75
|
+
# /title (`custom-title`) wins, else the auto-titler's name (`ai-title`). A brand
|
|
76
|
+
# -new session has neither, leaving the title empty; control's no-clobber guard
|
|
77
|
+
# keeps any prior name. See resolve_session_title in common.sh.
|
|
78
|
+
SESSION_TITLE="$(resolve_session_title "$TRANSCRIPT")"
|
|
79
|
+
|
|
80
|
+
LINE="$(jq -c -n \
|
|
81
|
+
--arg ts "$TS" --arg event "session_started" --arg key "$EVENT_KEY" \
|
|
82
|
+
--arg sessionId "$SESSION_ID" --arg transcript "$TRANSCRIPT" \
|
|
83
|
+
--arg cwd "$CWD" --arg branch "$BRANCH" --arg title "$SESSION_TITLE" \
|
|
84
|
+
'{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {transcriptPath: $transcript, repoPath: $cwd, branch: $branch, sessionTitle: $title}}')"
|
|
85
|
+
|
|
86
|
+
# Wedge v6 Epoch 35: repoPath sidecar. flush.sh is nohup-spawned by hooks,
|
|
87
|
+
# so its cwd is whatever nohup ran in (often $HOME) -- NOT the repo. Without
|
|
88
|
+
# this sidecar, `mla _internal finalize-session` falls back to process.cwd(),
|
|
89
|
+
# captureGitEvidence returns empty topLevel, the Epoch 33 guard refuses to
|
|
90
|
+
# POST, finalize_requested is re-spooled, and the next flush re-fails the
|
|
91
|
+
# same way. Permanent stuck-loss until the user manually runs `mla flush`
|
|
92
|
+
# from inside the repo. The sidecar captures the SessionStart $CWD (Claude
|
|
93
|
+
# Code fires the hook with cwd = the project root) so flush.sh can export
|
|
94
|
+
# MEETLESS_REPO_PATH for the CLI to consume. Written BEFORE spool_append so
|
|
95
|
+
# the detached flush sees it on first try.
|
|
96
|
+
printf '%s' "$CWD" > "$QUEUE_DIR/$SESSION_ID.repoPath"
|
|
97
|
+
|
|
98
|
+
# T1.2 hard cutover (folder = workspace): workspaceId sidecar. The marker is the
|
|
99
|
+
# ONLY source of the workspaceId, and the nohup-detached flusher cannot walk up
|
|
100
|
+
# to it (cwd=$HOME would always miss the repo marker). meetless_activated above
|
|
101
|
+
# resolved WORKSPACE_ID from this session's marker; snapshot it here so flush.sh
|
|
102
|
+
# wraps every POST under the marker id, never a stale cli-config value. Written
|
|
103
|
+
# BEFORE spool_append so the detached flush sees it on first try; flush.sh removes
|
|
104
|
+
# it after a successful finalize, alongside .repoPath and .gitBaseline.
|
|
105
|
+
printf '%s' "$WORKSPACE_ID" > "$QUEUE_DIR/$SESSION_ID.workspaceId"
|
|
106
|
+
|
|
107
|
+
# Wedge v6: git baseline sidecar. Records the working tree's dirty state at
|
|
108
|
+
# SESSION START so finalize can subtract ambient changes (files already
|
|
109
|
+
# modified/deleted/untracked before the agent ran) and attribute only what the
|
|
110
|
+
# SESSION touched. Without this, `mla review` blamed pre-existing dirty state
|
|
111
|
+
# (e.g. a stray `.claude/scheduled_tasks.lock` deletion) on the run. Same
|
|
112
|
+
# `-c core.quotePath=false` + `--porcelain=v1` form as captureGitEvidence so the
|
|
113
|
+
# exact-line subtraction matches. Best-effort: a non-repo $CWD writes an empty
|
|
114
|
+
# file, which subtracts nothing (back-compatible).
|
|
115
|
+
#
|
|
116
|
+
# 2026-06-01 dogfood finding F-GIT-1 (RCA 20260531 §9.F): capture the baseline
|
|
117
|
+
# ONCE per session. Claude Code re-fires SessionStart with the SAME session_id on
|
|
118
|
+
# a CONTINUE / COMPACTION / RESUME. The old unconditional write re-captured the
|
|
119
|
+
# baseline AFTER the prior turns' edits, freezing the agent's own work in as
|
|
120
|
+
# "ambient" -- subtractBaseline then dropped it and `mla review` showed
|
|
121
|
+
# "changed files: 0" on a session with real uncommitted edits. The guard below
|
|
122
|
+
# preserves the true-start snapshot across resumes. flush.sh removes the sidecar
|
|
123
|
+
# after a successful finalize, so the next genuine segment re-captures fresh
|
|
124
|
+
# (the absent file IS the "this is a new start" signal).
|
|
125
|
+
if [[ ! -e "$QUEUE_DIR/$SESSION_ID.gitBaseline" ]]; then
|
|
126
|
+
git -C "$CWD" -c core.quotePath=false status --porcelain=v1 \
|
|
127
|
+
> "$QUEUE_DIR/$SESSION_ID.gitBaseline" 2>/dev/null || \
|
|
128
|
+
: > "$QUEUE_DIR/$SESSION_ID.gitBaseline"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
spool_append "$SESSION_ID" "$LINE"
|
|
132
|
+
spawn_flush "$SESSION_ID"
|
|
133
|
+
|
|
134
|
+
# Sweep for Claude Code sessions whose transcript was deleted on disk and archive
|
|
135
|
+
# the mirrored AgentRun. Claude Code has no "session deleted" event, so SessionStart
|
|
136
|
+
# is the throttling tick for this disk-reconciliation sweep. Detached + kill-switched
|
|
137
|
+
# (MEETLESS_SESSION_RECONCILE=0), so it can never delay or fail the hook.
|
|
138
|
+
spawn_reconcile
|
|
139
|
+
|
|
140
|
+
exit 0
|