@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.
Files changed (202) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +81 -0
  3. package/dist/build-info.json +9 -0
  4. package/dist/bundles/ask-core.js +396 -0
  5. package/dist/bundles/mcp.js +16592 -0
  6. package/dist/bundles/trace-core.js +263 -0
  7. package/dist/cli.js +828 -0
  8. package/dist/commands/activate.js +781 -0
  9. package/dist/commands/adoption.js +130 -0
  10. package/dist/commands/ask.js +290 -0
  11. package/dist/commands/context.js +114 -0
  12. package/dist/commands/debug.js +313 -0
  13. package/dist/commands/doctor.js +1021 -0
  14. package/dist/commands/enrich.js +427 -0
  15. package/dist/commands/evidence.js +229 -0
  16. package/dist/commands/flush.js +184 -0
  17. package/dist/commands/graph.js +104 -0
  18. package/dist/commands/init.js +272 -0
  19. package/dist/commands/internal-active-review.js +322 -0
  20. package/dist/commands/internal-auto-index.js +188 -0
  21. package/dist/commands/internal-capture-decisions.js +320 -0
  22. package/dist/commands/internal-evidence-correlate.js +239 -0
  23. package/dist/commands/internal-evidence-hooks.js +240 -0
  24. package/dist/commands/internal-evidence-inject.js +231 -0
  25. package/dist/commands/internal-finalize.js +221 -0
  26. package/dist/commands/internal-pretool-observe.js +225 -0
  27. package/dist/commands/internal-refresh.js +136 -0
  28. package/dist/commands/internal-session-nudge.js +120 -0
  29. package/dist/commands/internal-steer-sync.js +117 -0
  30. package/dist/commands/internal-turn-recap.js +140 -0
  31. package/dist/commands/kb.js +375 -0
  32. package/dist/commands/kb_add.js +681 -0
  33. package/dist/commands/kb_forget.js +283 -0
  34. package/dist/commands/kb_move.js +45 -0
  35. package/dist/commands/kb_pending.js +410 -0
  36. package/dist/commands/kb_personal.js +149 -0
  37. package/dist/commands/kb_promote.js +188 -0
  38. package/dist/commands/kb_purge.js +168 -0
  39. package/dist/commands/kb_reingest.js +335 -0
  40. package/dist/commands/kb_retime.js +170 -0
  41. package/dist/commands/kb_review.js +391 -0
  42. package/dist/commands/kb_revision.js +179 -0
  43. package/dist/commands/kb_show.js +385 -0
  44. package/dist/commands/label.js +226 -0
  45. package/dist/commands/login.js +295 -0
  46. package/dist/commands/logout.js +108 -0
  47. package/dist/commands/mcp-supervisor.js +93 -0
  48. package/dist/commands/mcp.js +227 -0
  49. package/dist/commands/queue-prune.js +98 -0
  50. package/dist/commands/review.js +358 -0
  51. package/dist/commands/rewire.js +124 -0
  52. package/dist/commands/rules.js +728 -0
  53. package/dist/commands/scan-context.js +67 -0
  54. package/dist/commands/session.js +347 -0
  55. package/dist/commands/stats.js +479 -0
  56. package/dist/commands/status.js +61 -0
  57. package/dist/commands/summary.js +250 -0
  58. package/dist/commands/turn.js +114 -0
  59. package/dist/commands/uninstall.js +222 -0
  60. package/dist/commands/whoami.js +102 -0
  61. package/dist/commands/workspace.js +130 -0
  62. package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
  63. package/dist/hooks-template/ce0-session-start.sh +49 -0
  64. package/dist/hooks-template/ce0-stop.sh +29 -0
  65. package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
  66. package/dist/hooks-template/common.sh +934 -0
  67. package/dist/hooks-template/event-batch-filter.jq +67 -0
  68. package/dist/hooks-template/flush.sh +503 -0
  69. package/dist/hooks-template/post-tool-use.sh +423 -0
  70. package/dist/hooks-template/pre-tool-use.sh +69 -0
  71. package/dist/hooks-template/session-start.sh +140 -0
  72. package/dist/hooks-template/stop.sh +308 -0
  73. package/dist/hooks-template/user-prompt-submit.sh +1162 -0
  74. package/dist/lib/activation.js +79 -0
  75. package/dist/lib/active-conflict-cache.js +141 -0
  76. package/dist/lib/active-memory.js +59 -0
  77. package/dist/lib/active-review-runner.js +26 -0
  78. package/dist/lib/agent-decision/index.js +25 -0
  79. package/dist/lib/agent-decision/keys.js +49 -0
  80. package/dist/lib/agent-decision/normalize-claude.js +183 -0
  81. package/dist/lib/agent-decision/types.js +21 -0
  82. package/dist/lib/agent-decision/validate.js +216 -0
  83. package/dist/lib/analytics/capture.js +96 -0
  84. package/dist/lib/analytics/command-event.js +267 -0
  85. package/dist/lib/analytics/consent.js +58 -0
  86. package/dist/lib/analytics/coverage-gap.js +96 -0
  87. package/dist/lib/analytics/envelope.js +236 -0
  88. package/dist/lib/analytics/event-id.js +86 -0
  89. package/dist/lib/analytics/evidence.js +150 -0
  90. package/dist/lib/analytics/followthrough.js +194 -0
  91. package/dist/lib/analytics/forwarder.js +109 -0
  92. package/dist/lib/analytics/logs.js +78 -0
  93. package/dist/lib/analytics/metrics.js +78 -0
  94. package/dist/lib/analytics/recorder.js +92 -0
  95. package/dist/lib/analytics/review-analytics.js +75 -0
  96. package/dist/lib/analytics/sequence.js +77 -0
  97. package/dist/lib/analytics/store.js +131 -0
  98. package/dist/lib/analytics/turn-recap.js +279 -0
  99. package/dist/lib/artifact_id.js +108 -0
  100. package/dist/lib/auth-breaker.js +161 -0
  101. package/dist/lib/auto-index.js +112 -0
  102. package/dist/lib/classifier.js +88 -0
  103. package/dist/lib/config.js +298 -0
  104. package/dist/lib/conflict-advisory.js +64 -0
  105. package/dist/lib/debug-bundle.js +520 -0
  106. package/dist/lib/enrichment/ingest.js +301 -0
  107. package/dist/lib/enrichment/plan.js +253 -0
  108. package/dist/lib/enrichment/protocol.js +359 -0
  109. package/dist/lib/enrichment/scout-brief.js +176 -0
  110. package/dist/lib/failure-telemetry.js +444 -0
  111. package/dist/lib/git.js +200 -0
  112. package/dist/lib/governance-cache.js +77 -0
  113. package/dist/lib/governed-path-cache.js +76 -0
  114. package/dist/lib/http.js +677 -0
  115. package/dist/lib/identity-envelope.js +23 -0
  116. package/dist/lib/kb-candidate.js +65 -0
  117. package/dist/lib/kb_acl.js +98 -0
  118. package/dist/lib/login.js +353 -0
  119. package/dist/lib/mcp-fetchers.js +130 -0
  120. package/dist/lib/mcp-restart.js +47 -0
  121. package/dist/lib/observability.js +805 -0
  122. package/dist/lib/open-url.js +33 -0
  123. package/dist/lib/orphan-guard.js +70 -0
  124. package/dist/lib/packaged.js +21 -0
  125. package/dist/lib/reconcile-sessions.js +171 -0
  126. package/dist/lib/redactor.js +89 -0
  127. package/dist/lib/relationship-candidate-query.js +27 -0
  128. package/dist/lib/render.js +611 -0
  129. package/dist/lib/rules/applicability.js +64 -0
  130. package/dist/lib/rules/attest-code-rule-version.js +47 -0
  131. package/dist/lib/rules/attest-notes-location.js +217 -0
  132. package/dist/lib/rules/attest-rule-version.js +69 -0
  133. package/dist/lib/rules/canonical-json.js +97 -0
  134. package/dist/lib/rules/ce0-emit.js +64 -0
  135. package/dist/lib/rules/ce0-evidence.js +281 -0
  136. package/dist/lib/rules/ce0-recall-sample.js +82 -0
  137. package/dist/lib/rules/ce0-rule.js +55 -0
  138. package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
  139. package/dist/lib/rules/ce0-store.js +683 -0
  140. package/dist/lib/rules/ce0-telemetry-project.js +93 -0
  141. package/dist/lib/rules/ce0-telemetry.js +158 -0
  142. package/dist/lib/rules/code-rule-registry.js +17 -0
  143. package/dist/lib/rules/command-match.js +185 -0
  144. package/dist/lib/rules/consult-evidence-binding.js +27 -0
  145. package/dist/lib/rules/consultation-capture-adapter.js +193 -0
  146. package/dist/lib/rules/content-match.js +56 -0
  147. package/dist/lib/rules/deny-admission.js +99 -0
  148. package/dist/lib/rules/durable-observation.js +190 -0
  149. package/dist/lib/rules/enforce-notes-version.js +421 -0
  150. package/dist/lib/rules/evaluation-input-hash.js +126 -0
  151. package/dist/lib/rules/evaluator.js +108 -0
  152. package/dist/lib/rules/inert-rule-families.js +51 -0
  153. package/dist/lib/rules/input-authority-resolver.js +241 -0
  154. package/dist/lib/rules/interception-schema.js +170 -0
  155. package/dist/lib/rules/interception-store.js +267 -0
  156. package/dist/lib/rules/live-input-authority.js +66 -0
  157. package/dist/lib/rules/local-matcher.js +108 -0
  158. package/dist/lib/rules/local-observe.js +79 -0
  159. package/dist/lib/rules/local-rule-version-repo.js +214 -0
  160. package/dist/lib/rules/memory-requirement.js +109 -0
  161. package/dist/lib/rules/notes-observe.js +39 -0
  162. package/dist/lib/rules/notes-path.js +261 -0
  163. package/dist/lib/rules/notes-rule.js +75 -0
  164. package/dist/lib/rules/observe-adapter.js +114 -0
  165. package/dist/lib/rules/observed-rule-hash.js +119 -0
  166. package/dist/lib/rules/prompt-submit-adapter.js +132 -0
  167. package/dist/lib/rules/requirement-subject.js +240 -0
  168. package/dist/lib/rules/rule-activity.js +67 -0
  169. package/dist/lib/rules/rule-version-hash.js +151 -0
  170. package/dist/lib/rules/runtime-scope.js +55 -0
  171. package/dist/lib/rules/stop-adapter.js +116 -0
  172. package/dist/lib/rules/stop-response-snapshot.js +174 -0
  173. package/dist/lib/rules/types.js +10 -0
  174. package/dist/lib/rules/ulid.js +46 -0
  175. package/dist/lib/rules/version-evaluation.js +156 -0
  176. package/dist/lib/scanner/agent-memory.js +99 -0
  177. package/dist/lib/scanner/bootstrap-summary.js +87 -0
  178. package/dist/lib/scanner/cache.js +59 -0
  179. package/dist/lib/scanner/frontmatter.js +42 -0
  180. package/dist/lib/scanner/parse-directives.js +69 -0
  181. package/dist/lib/scanner/parse-structured.js +72 -0
  182. package/dist/lib/scanner/render.js +73 -0
  183. package/dist/lib/scanner/scan.js +132 -0
  184. package/dist/lib/scanner/score.js +38 -0
  185. package/dist/lib/scanner/scout-mission.js +126 -0
  186. package/dist/lib/scanner/types.js +7 -0
  187. package/dist/lib/session-scope.js +195 -0
  188. package/dist/lib/spool.js +355 -0
  189. package/dist/lib/staleness.js +100 -0
  190. package/dist/lib/steer-cache.js +87 -0
  191. package/dist/lib/tagged-reference.js +20 -0
  192. package/dist/lib/temporal.js +109 -0
  193. package/dist/lib/turn-recap-emit.js +67 -0
  194. package/dist/lib/unwire.js +253 -0
  195. package/dist/lib/update-check.js +469 -0
  196. package/dist/lib/update-notifier.js +217 -0
  197. package/dist/lib/upgrade-apply.js +643 -0
  198. package/dist/lib/wire.js +1087 -0
  199. package/dist/lib/workspace.js +96 -0
  200. package/dist/lib/zip.js +154 -0
  201. package/dist/pretool-entry.js +37 -0
  202. package/package.json +75 -0
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env bash
2
+ # stop.sh: Claude Code Stop hook. Writes session_stopped + finalize_requested
3
+ # events to the spool and spawns the flusher. Stop must return in <1s.
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). Exit before any work unless a
9
+ # `.meetless.json` marker is found by walking up from $PWD. See
10
+ # meetless_activated in common.sh. Run `mla activate` in a repo to opt in.
11
+ meetless_activated || exit 0
12
+
13
+ INPUT="$(cat)"
14
+ # Wedge v6 Epoch 29: validate stdin parses as JSON BEFORE any jq substitution.
15
+ # Stop is the critical path for review-packet creation; a single malformed
16
+ # Claude Code payload here silently kills the finalize_requested + flush
17
+ # pipeline and no review is ever generated. See session-start.sh.
18
+ if [[ -z "$INPUT" ]] || ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
19
+ exit 0
20
+ fi
21
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"
22
+ [[ -z "$SESSION_ID" ]] && exit 0
23
+ # Per-session OFF override (`mla deactivate`). Silences this one session even in
24
+ # an activated folder. See meetless_session_disabled in common.sh.
25
+ meetless_session_disabled "$SESSION_ID" && exit 0
26
+
27
+ TRANSCRIPT="$(echo "$INPUT" | jq -r '.transcript_path // empty')"
28
+ TS="$(date -u +%FT%TZ)"
29
+
30
+ # Transcript-flush settle (Bug B / Q6 race). Claude Code can fire Stop a beat
31
+ # BEFORE the turn's CLOSING assistant message is flushed to the transcript file.
32
+ # The reads below then grab a MID-TURN text block as finalMessage and the
33
+ # narration slice inherits the same wrong boundary.
34
+ #
35
+ # a6b36c66's first cut polled the transcript byte-SIZE until it stopped growing.
36
+ # That has a residual hole the dogfood audit of session 5d428e3e hit head-on:
37
+ # byte-size stability cannot tell "the turn finished" from "the single closing
38
+ # append has not landed yet". If the writer goes quiet for one poll interval
39
+ # before flushing the closing message as one append, the byte-size settle breaks
40
+ # early and the extraction still lands on the stale mid-turn block (live: stored
41
+ # "Control owns it (graph service). Let me read the actual def." instead of the
42
+ # real closing answer "Pulled the canonical model and the actual code path...").
43
+ #
44
+ # Modern Claude Code transcripts stamp every assistant entry with a stop_reason:
45
+ # mid-turn blocks that precede a tool call carry "tool_use"; the turn's CLOSING
46
+ # message carries "end_turn". So gate the settle on that SEMANTIC boundary, not
47
+ # on bytes: wait until an end_turn assistant entry with non-whitespace text
48
+ # exists ("ready"), poll while modern entries exist but none is closed yet
49
+ # ("wait"), and only fall back to byte-size stability for LEGACY transcripts that
50
+ # carry no stop_reason at all ("legacy"). Bounded under the <1s Stop budget; the
51
+ # common already-flushed case breaks on the first check with zero sleeps.
52
+ # Fail-soft: a never-closing file just hits the attempt cap and the extraction
53
+ # below falls back to the last text block. Tunable via env for tests.
54
+ _settle_verdict_jq='
55
+ split("\n")
56
+ | map(select(length > 0) | fromjson?)
57
+ | [ .[] | select(.type == "assistant") ] as $a
58
+ | ([ $a[] | select(.message | has("stop_reason")) ] | length) as $modern
59
+ | ([ $a[]
60
+ | select(.message.stop_reason == "end_turn")
61
+ | select((.message.content // [])
62
+ | any(.type == "text" and ((.text // "") | gsub("\\s"; "") | length > 0))) ]
63
+ | length) as $closed
64
+ | if $modern > 0 then (if $closed > 0 then "ready" else "wait" end) else "legacy" end
65
+ '
66
+ if [[ -n "$TRANSCRIPT" && -f "$TRANSCRIPT" ]]; then
67
+ _settle_poll="${MEETLESS_FINALMSG_POLL_SEC:-0.06}"
68
+ _settle_max="${MEETLESS_FINALMSG_MAX_ATTEMPTS:-10}"
69
+ _settle_prev="-1"
70
+ _settle_i=0
71
+ while [ "$_settle_i" -lt "$_settle_max" ]; do
72
+ _settle_verdict="$(tail -n 400 "$TRANSCRIPT" 2>/dev/null \
73
+ | jq -rR --slurp "$_settle_verdict_jq" 2>/dev/null || echo legacy)"
74
+ if [[ "$_settle_verdict" == "ready" ]]; then
75
+ break
76
+ elif [[ "$_settle_verdict" == "legacy" ]]; then
77
+ # No stop_reason anywhere: fall back to a6b36c66's byte-size stability.
78
+ _settle_size="$(wc -c < "$TRANSCRIPT" 2>/dev/null | tr -d ' ' || true)"
79
+ [[ -z "$_settle_size" ]] && _settle_size=0
80
+ if [[ "$_settle_size" == "$_settle_prev" ]]; then break; fi
81
+ _settle_prev="$_settle_size"
82
+ fi
83
+ # "wait" (modern, end_turn not flushed yet) or "legacy still growing": poll.
84
+ sleep "$_settle_poll" 2>/dev/null || true
85
+ _settle_i=$((_settle_i + 1))
86
+ done
87
+ fi
88
+
89
+ # Best-effort closing assistant message (Q6: option A; transcript-flush settled above).
90
+ #
91
+ # Modern Claude Code transcripts have shape `{type: "assistant", message: {content: [{type: "text", text: "..."}, {type: "tool_use", ...}], stop_reason: "..."}}`.
92
+ # The legacy `.content // .message` fallback caught the whole envelope object
93
+ # and `tostring`'d the JSON dump into finalMessage (model, id, content blocks),
94
+ # which poisoned `agentClaimsRaw` and the intel synthesizer's view of what the
95
+ # agent claimed it did.
96
+ #
97
+ # Pick the turn's CLOSING message, not merely "the last assistant text block".
98
+ # The two diverge whenever a non-end_turn assistant entry trails the closing one
99
+ # (a stale mid-turn tool_use block during the pre-flush gap, or a trailing
100
+ # continuation artifact): "last text block" grabs the wrong one. So prefer the
101
+ # last assistant entry whose stop_reason is "end_turn" (the semantic turn
102
+ # boundary), and only fall back to the last text block for LEGACY transcripts
103
+ # with no stop_reason at all. Join its text blocks.
104
+ FINAL_MSG=""
105
+ if [[ -n "$TRANSCRIPT" && -f "$TRANSCRIPT" ]]; then
106
+ FINAL_MSG="$(tail -n 400 "$TRANSCRIPT" 2>/dev/null \
107
+ | jq -rR --slurp '
108
+ split("\n")
109
+ | map(select(length > 0) | fromjson?)
110
+ | [ .[] | select(.type == "assistant")
111
+ | select((.message.content // []) | any(.type == "text")) ]
112
+ | ((map(select(.message.stop_reason == "end_turn")) | last) // last) as $pick
113
+ | if $pick == null then ""
114
+ else ($pick.message.content // [])
115
+ | map(select(.type == "text") | .text // "")
116
+ | join("\n")
117
+ end
118
+ ' 2>/dev/null || true)"
119
+ fi
120
+
121
+ # Best-effort intra-turn narration (full-prose replay; note 20260610 §4 P3 step
122
+ # 11, capture-scope option B). FINAL_MSG above is the agent's LAST assistant
123
+ # message (the closing summary). NARRATION is everything the agent SAID earlier
124
+ # in THIS turn, between tool calls, which the timeline replay was otherwise
125
+ # missing. Turn-bounding is the subtle part: tool_result entries are user-role
126
+ # too, so we cannot slice at "the last user entry" or we would cut mid-turn.
127
+ # Instead we find the last REAL user prompt (string content, or an array with no
128
+ # tool_result block) and take the assistant text entries AFTER it, dropping the
129
+ # LAST one (that is FINAL_MSG, so it is never double-counted). Empty (no
130
+ # narration, or only a closing summary) means no event is spooled. Narration is
131
+ # the default now (no kill switch); the post-tool-use hook captures it LIVE and
132
+ # this Stop pass is the compaction-safe backstop.
133
+ NARRATION=""
134
+ if [[ -n "$TRANSCRIPT" && -f "$TRANSCRIPT" ]]; then
135
+ NARRATION="$(tail -n 400 "$TRANSCRIPT" 2>/dev/null \
136
+ | jq -rR --slurp '
137
+ split("\n")
138
+ | map(select(length > 0) | fromjson?)
139
+ | . as $rows
140
+ | (reduce range(0; ($rows | length)) as $i (-1;
141
+ if ($rows[$i].type == "user")
142
+ and (
143
+ (($rows[$i].message.content | type) == "string")
144
+ or (
145
+ (($rows[$i].message.content // []) | type) == "array"
146
+ and (($rows[$i].message.content // []) | any(.type == "tool_result") | not)
147
+ )
148
+ )
149
+ then $i else . end
150
+ )) as $start
151
+ | (if $start < 0 then [] else $rows[($start + 1):] end)
152
+ | [ .[] | select(.type == "assistant")
153
+ | select((.message.content // []) | any(.type == "text")) ] as $texts
154
+ | ($texts | length) as $n
155
+ # Drop the SAME entry FINAL_MSG selected as the closing message: the last
156
+ # end_turn block, else the last text block (legacy). Dropping by INDEX,
157
+ # not by value, so two identical-content blocks do not both vanish; for a
158
+ # legacy transcript this is exactly the old `.[0:-1]` slice.
159
+ | (([ range(0; $n) | select($texts[.].message.stop_reason == "end_turn") ] | last) // ($n - 1)) as $ci
160
+ | [ range(0; $n) | select(. != $ci) | $texts[.] ]
161
+ | map((.message.content // [])
162
+ | map(select(.type == "text") | .text // "")
163
+ | join("\n"))
164
+ | map(select((gsub("\\s"; "") | length) > 0))
165
+ | join("\n\n")
166
+ ' 2>/dev/null || true)"
167
+ fi
168
+
169
+ # Best-effort current session name. Mirrors the local picker: human /title
170
+ # (`custom-title`) wins, else the auto-titler's name (`ai-title`). Carrying it on
171
+ # session_stopped lets control track renames last-write-wins. See
172
+ # resolve_session_title in common.sh for the precedence + fail-soft contract.
173
+ SESSION_TITLE="$(resolve_session_title "$TRANSCRIPT")"
174
+
175
+ # ---- report-citation capture (P3) ---------------------------------------
176
+ # Parse the [XX:id] evidence tokens the agent's FINAL report cited and record
177
+ # them LOCALLY, keyed by (session_id, turn_index). This is the push-reference
178
+ # side of A1b: "did the agent's final report cite a source_id we injected, even
179
+ # with no Pull?". It is a local sibling of mcp-calls.jsonl (the pull side, P1)
180
+ # and ask-traces.jsonl (the inject side), so the A1 evidence-followthrough join
181
+ # stays a purely local reader. The turn counter is READ, never advanced
182
+ # (UserPromptSubmit owns it); Stop fires at the end of turn N's response while
183
+ # the counter still holds N. An empty array is recorded too: "this turn's report
184
+ # cited nothing" is a real A1b denominator signal. extract_source_ids (common.sh)
185
+ # is the single shared grammar with the pull side.
186
+ # notes/20260603-mla-kb-agent-proxy-and-evidence-adoption.md §7.1 P3 / §7.4 A1.
187
+ mkdir -p "$QUEUE_DIR" "$LOG_DIR"
188
+ REPORT_TURN="$(current_turn_index "$SESSION_ID")"
189
+ REPORT_SIDS="$(extract_source_ids "$FINAL_MSG")"
190
+ REPORT_LINE="$(jq -c -n \
191
+ --arg ts "$TS" --arg event "report_citations" \
192
+ --arg sessionId "$SESSION_ID" --argjson turn "$REPORT_TURN" \
193
+ --argjson sids "$REPORT_SIDS" \
194
+ '{ts: $ts, event: $event, session_id: $sessionId, turn_index: $turn, source_ids: $sids}')"
195
+ (
196
+ flock 9
197
+ printf '%s\n' "$REPORT_LINE" >> "$LOG_DIR/report-citations.jsonl"
198
+ ) 9>"$LOG_DIR/report-citations.lock"
199
+
200
+ # End-of-run review card: surface up to 5 deterministic stale signals to the user.
201
+ # P0A-minimal: written to a LOCAL jsonl only (review_card is not in the flush
202
+ # allowlist), later surfaced by `mla status` / `mla context list`. Cheap jq read of
203
+ # the scan cache; never recomputes the scan. Always exits 0 so it cannot abort Stop.
204
+ build_stop_review_card() {
205
+ local cache="$HOME/.meetless/workspaces/$WORKSPACE_ID/scan-cache.json"
206
+ [[ -r "$cache" ]] || return 0
207
+ jq -c -n \
208
+ --slurpfile c "$cache" \
209
+ --arg sid "$SESSION_ID" \
210
+ --arg ts "$TS" \
211
+ '{
212
+ ts: $ts, event: "review_card", session_id: $sid,
213
+ items: ($c[0].staleSignals // [])[0:5] | map({id: .id, detail: .detail, source: .source}),
214
+ total: (($c[0].staleSignals // []) | length)
215
+ }' 2>/dev/null || true
216
+ }
217
+
218
+ REVIEW_CARD_LINE="$(build_stop_review_card)"
219
+ if [[ -n "$REVIEW_CARD_LINE" ]]; then
220
+ printf '%s\n' "$REVIEW_CARD_LINE" >> "$HOME/.meetless/workspaces/$WORKSPACE_ID/review-cards.jsonl" 2>/dev/null || true
221
+ fi
222
+
223
+ STOPPED_KEY="$(gen_event_key)"
224
+ LINE_STOPPED="$(jq -c -n \
225
+ --arg ts "$TS" --arg event "session_stopped" --arg key "$STOPPED_KEY" \
226
+ --arg sessionId "$SESSION_ID" --arg final "$FINAL_MSG" --arg title "$SESSION_TITLE" \
227
+ '{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {finalMessage: $final, sessionTitle: $title}}')"
228
+
229
+ FINALIZE_KEY="$(gen_event_key)"
230
+ LINE_FINALIZE="$(jq -c -n \
231
+ --arg ts "$TS" --arg event "finalize_requested" --arg key "$FINALIZE_KEY" \
232
+ --arg sessionId "$SESSION_ID" \
233
+ '{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {}}')"
234
+
235
+ # Spool the intra-turn narration FIRST (before session_stopped) so it sorts ahead
236
+ # of "Session ended" in control's occurredAt-asc, id-asc timeline: same TS, lower
237
+ # row id. Only when there is actually narration to show (empty stays unspooled).
238
+ if [[ -n "$NARRATION" ]]; then
239
+ NARRATION_KEY="$(gen_event_key)"
240
+ LINE_NARRATION="$(jq -c -n \
241
+ --arg ts "$TS" --arg event "assistant_message" --arg key "$NARRATION_KEY" \
242
+ --arg sessionId "$SESSION_ID" --arg narration "$NARRATION" \
243
+ '{ts: $ts, event: $event, eventKey: $key, sessionId: $sessionId, payload: {narration: $narration}}')"
244
+ spool_append "$SESSION_ID" "$LINE_NARRATION"
245
+ fi
246
+
247
+ spool_append "$SESSION_ID" "$LINE_STOPPED"
248
+ spool_append "$SESSION_ID" "$LINE_FINALIZE"
249
+
250
+ # ---- AskUserQuestion agent-decision backstop (stop transcript scan) ------
251
+ # The PostToolUse primary path (post-tool-use.sh) captures each answered
252
+ # AskUserQuestion in real time, but a hook that never fired (crash, race, a
253
+ # session that predates the matcher) would lose the decision. The Stop hook is
254
+ # the guaranteed backstop: scan THIS session's transcript for AskUserQuestion
255
+ # tool_use / tool_result pairs and spool any decision the primary path missed.
256
+ # A fast `grep -q` gate skips the scan cost entirely on the common no-question
257
+ # session. Both paths derive the SAME providerEventId, and --spool dedups against
258
+ # what the primary already queued, so a doubly-captured decision is spooled once.
259
+ # Spooled BEFORE spawn_flush so the decisions ride this same flush cycle.
260
+ # Fail-soft: a missing mla binary, an unreadable transcript, or a command error
261
+ # is swallowed and never delays or fails Stop (<1s budget).
262
+ # See notes/20260608-agent-decision-capture-design.md section 5.
263
+ if [[ -n "${MLA_PATH:-}" && -x "$MLA_PATH" && -n "$TRANSCRIPT" && -f "$TRANSCRIPT" ]] \
264
+ && grep -q "AskUserQuestion" "$TRANSCRIPT" 2>/dev/null; then
265
+ DECISION_LINES="$("$MLA_PATH" _internal capture-decisions \
266
+ --source stop_transcript_scan --transcript "$TRANSCRIPT" \
267
+ --session "$SESSION_ID" --spool "$QUEUE_DIR/$SESSION_ID.jsonl" 2>/dev/null || true)"
268
+ while IFS= read -r DECISION_LINE; do
269
+ [[ -z "$DECISION_LINE" ]] && continue
270
+ spool_append "$SESSION_ID" "$DECISION_LINE"
271
+ done <<< "$DECISION_LINES"
272
+ fi
273
+
274
+ spawn_flush "$SESSION_ID"
275
+
276
+ # Hands-off stale-session GC: sweep dead-session litter idle > 24h. Detached and
277
+ # reap-only (no re-drain), so it never blocks Stop and never re-flushes the live
278
+ # sessions. This session's own spool is handled by spawn_flush above.
279
+ spawn_reap
280
+
281
+ # Zone 2 auto-index: index THIS session's produced prose docs into the owner's
282
+ # Personal KB as SHADOW (never grounds anyone; INV-GROUNDING-APPROVED). Detached,
283
+ # fail-soft, and kill-switchable (MEETLESS_AUTO_INDEX=0), so it never blocks Stop.
284
+ # Runs after the reap so it rides the same end-of-Stop tail without delaying GC.
285
+ spawn_auto_index "$SESSION_ID"
286
+
287
+ # T4.2 evidence-outcome correlator (INV-CORRELATOR-1): close every eligible PENDING
288
+ # inject window (3 turns or 15 minutes) across ALL sessions and append one
289
+ # mla_evidence_outcome per closed inject to the local jsonl, then forward if
290
+ # telemetry is on. Detached, fail-soft, and kill-switchable (MEETLESS_EVIDENCE_ANALYTICS=0),
291
+ # so it never blocks Stop. Rides the same end-of-Stop tail as the auto-index above.
292
+ # No session argument: it sweeps cross-session because a window can close minutes
293
+ # after the originating session ended, and a Stop is the natural recompute tick.
294
+ spawn_evidence_correlate
295
+
296
+ # Layer D per-turn recap -> Langfuse: post THIS just-finished turn's assist recap
297
+ # to intel so it attaches the mla_ran / mla_assist scores + the full recap metadata
298
+ # to the turn's Langfuse trace (keyed on the per-turn $TRACE_ID intel adopts as the
299
+ # langfuse_trace_id). REPORT_TURN computed above is the just-finished turn N (the
300
+ # counter still holds N at Stop; UserPromptSubmit owns advancing it). Detached,
301
+ # fail-soft, and kill-switchable via MEETLESS_TURN_RECAP_LANGFUSE=off (its OWN flag,
302
+ # independent of the C-lite injection's MEETLESS_TURN_RECAP), so it never blocks
303
+ # Stop. Rides the same end-of-Stop tail as the kickoffs above.
304
+ # See notes/20260609-mla-per-turn-assist-recap-plan.md §4.4.
305
+ spawn_turn_recap_emit "$SESSION_ID" "$REPORT_TURN"
306
+
307
+ # Stop returns in <1s. Worker runs review async.
308
+ exit 0