@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,195 @@
1
+ "use strict";
2
+ // Resolve a session value into (a) the concrete sid to scope to and (b) the set of
3
+ // note keys that session produced IN A GIVEN WORKSPACE, read from the active-memory
4
+ // store the Zone-2 auto-index loop writes (see internal-auto-index.ts). Shared by
5
+ // the kb review listing (session default) and any other session-scoped surface.
6
+ //
7
+ // The join key is the note BASENAME. Verified live 2026-06-07 against control:
8
+ // candidate artifactIds are `note:<basename>` while the store records canonicalPath
9
+ // as `notes/<basename>`. The route DTO documents notePath as a bare basename. So
10
+ // the one normalization that makes the store join the candidate graph is
11
+ // basename(canonicalPath).
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.SCOPE_MAX_RECORDS = exports.SCOPE_TTL_HOURS = exports.SessionScopeError = void 0;
47
+ exports.noteKey = noteKey;
48
+ exports.noteArtifactId = noteArtifactId;
49
+ exports.activeMemoryStorePath = activeMemoryStorePath;
50
+ exports.resolveScopeSession = resolveScopeSession;
51
+ exports.sessionNoteKeys = sessionNoteKeys;
52
+ exports.candidateInSession = candidateInSession;
53
+ exports.loadSessionScope = loadSessionScope;
54
+ const fs = __importStar(require("fs"));
55
+ const path = __importStar(require("path"));
56
+ const config_1 = require("./config");
57
+ const active_memory_1 = require("./active-memory");
58
+ function noteKey(canonicalPath) {
59
+ // Normalize Windows separators defensively before taking the basename; the
60
+ // store is written on macOS/Linux today, so this is free correctness, not a
61
+ // supported-platform path.
62
+ return path.posix.basename(canonicalPath.replace(/\\/g, "/"));
63
+ }
64
+ function noteArtifactId(canonicalPath) {
65
+ return `note:${noteKey(canonicalPath)}`;
66
+ }
67
+ class SessionScopeError extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "SessionScopeError";
71
+ // Restore the prototype chain so `instanceof` survives ts-jest's ES target.
72
+ Object.setPrototypeOf(this, SessionScopeError.prototype);
73
+ }
74
+ }
75
+ exports.SessionScopeError = SessionScopeError;
76
+ function activeMemoryStorePath() {
77
+ return path.join(config_1.HOME, "logs", "kb-knowledge.jsonl");
78
+ }
79
+ // Wider window than the auto-index loop (48h/100): a human reviewing "this
80
+ // session" may run the command well after the work landed, and the store is small.
81
+ exports.SCOPE_TTL_HOURS = 168; // 7 days
82
+ exports.SCOPE_MAX_RECORDS = 1000;
83
+ function resolveScopeSession(value, deps = {}) {
84
+ const v = value.trim();
85
+ if (v === "") {
86
+ throw new SessionScopeError("--session requires a value (a sid, or 'current' / 'latest')");
87
+ }
88
+ if (v !== "current" && v !== "latest") {
89
+ return { sessionId: v, source: "explicit" };
90
+ }
91
+ if (v === "current") {
92
+ const env = deps.env ?? process.env;
93
+ const sid = (env.CLAUDE_CODE_SESSION_ID || "").trim();
94
+ if (!sid) {
95
+ // NEVER fall back to "latest" silently; that would bind to a different
96
+ // session than the one the operator is in.
97
+ throw new SessionScopeError("--session current needs $CLAUDE_CODE_SESSION_ID, which is not set. " +
98
+ "Run inside a Claude Code session, pass an explicit sid, or use --session latest.");
99
+ }
100
+ return { sessionId: sid, source: "current-env" };
101
+ }
102
+ // v === "latest": explicit, loud opt-in to cross-session selection.
103
+ const storePath = deps.storePath ?? activeMemoryStorePath();
104
+ const sid = latestProducedDocSession(storePath, deps.workspaceId ?? null);
105
+ if (!sid) {
106
+ throw new SessionScopeError(`--session latest found no session that produced docs` +
107
+ `${deps.workspaceId ? ` in workspace ${deps.workspaceId}` : ""}. Pass an explicit sid.`);
108
+ }
109
+ return { sessionId: sid, source: "latest-store" };
110
+ }
111
+ // Latest session that PRODUCED a doc (the queue is scoped by produced docs, so a
112
+ // session whose newest event was a tagged_reference is not a useful "latest").
113
+ // Workspace-scoped so a foreign repo's session never wins.
114
+ function latestProducedDocSession(storePath, workspaceId) {
115
+ let raw;
116
+ try {
117
+ raw = fs.readFileSync(storePath, "utf8");
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ let best = null;
123
+ for (const line of raw.split("\n")) {
124
+ const s = line.trim();
125
+ if (!s)
126
+ continue;
127
+ let r;
128
+ try {
129
+ r = JSON.parse(s);
130
+ }
131
+ catch {
132
+ continue;
133
+ }
134
+ if (r.event !== "active_memory_record" || r.kind !== "produced_doc" || !r.sessionId)
135
+ continue;
136
+ if (workspaceId && r.workspaceId !== workspaceId)
137
+ continue;
138
+ const parsed = Date.parse(r.createdAt ?? r.ts ?? "");
139
+ // A record with no usable date cannot meaningfully be "latest"; skip it rather
140
+ // than letting a corrupt timestamp win by defaulting to epoch 0.
141
+ if (!Number.isFinite(parsed))
142
+ continue;
143
+ if (!best || parsed >= best.t)
144
+ best = { sid: r.sessionId, t: parsed };
145
+ }
146
+ return best?.sid ?? null;
147
+ }
148
+ // Reduce the active-memory store to the produced-doc note keys for one session in
149
+ // one workspace. reduceActiveMemory returns [] on a missing file and filters to
150
+ // event === "active_memory_record" + sessionId, so we only add the workspace +
151
+ // kind filters here.
152
+ function sessionNoteKeys(sessionId, deps) {
153
+ const storePath = deps.storePath ?? activeMemoryStorePath();
154
+ const records = (0, active_memory_1.reduceActiveMemory)(storePath, {
155
+ sessionId,
156
+ ttlHours: exports.SCOPE_TTL_HOURS,
157
+ maxRecords: exports.SCOPE_MAX_RECORDS,
158
+ nowMs: deps.nowMs,
159
+ });
160
+ const keys = new Set();
161
+ for (const r of records) {
162
+ if (r.kind !== "produced_doc")
163
+ continue;
164
+ if (r.workspaceId !== deps.workspaceId)
165
+ continue;
166
+ keys.add(noteKey(r.canonicalPath));
167
+ }
168
+ return { sessionId, keys };
169
+ }
170
+ function stripNotePrefix(artifactId) {
171
+ if (!artifactId)
172
+ return null;
173
+ return artifactId.startsWith("note:") ? artifactId.slice("note:".length) : null;
174
+ }
175
+ // A candidate belongs to the session if EITHER endpoint is a note the session
176
+ // produced (the detected edge can put the new doc on the source or target side).
177
+ function candidateInSession(c, keys) {
178
+ const s = stripNotePrefix(c.sourceArtifactId);
179
+ const t = stripNotePrefix(c.targetArtifactId);
180
+ return (s !== null && keys.has(s)) || (t !== null && keys.has(t));
181
+ }
182
+ // Convenience used by the command entrypoints: resolve the value, then load keys.
183
+ function loadSessionScope(value, deps) {
184
+ const resolved = resolveScopeSession(value, {
185
+ env: deps.env,
186
+ storePath: deps.storePath,
187
+ workspaceId: deps.workspaceId,
188
+ });
189
+ const scope = sessionNoteKeys(resolved.sessionId, {
190
+ workspaceId: deps.workspaceId,
191
+ storePath: deps.storePath,
192
+ nowMs: deps.nowMs,
193
+ });
194
+ return { sessionId: resolved.sessionId, source: resolved.source, keys: scope.keys };
195
+ }
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.listActiveSessions = listActiveSessions;
37
+ exports.queueDepth = queueDepth;
38
+ exports.reapQueue = reapQueue;
39
+ exports.planQueuePrune = planQueuePrune;
40
+ exports.executeQueuePrune = executeQueuePrune;
41
+ exports.runFlushScript = runFlushScript;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const child_process_1 = require("child_process");
45
+ const config_1 = require("./config");
46
+ // Spool helpers used by `mla flush` to scan and drain pending queue files.
47
+ // The actual flush mechanic lives in ~/.meetless/hooks/flush.sh; we call it
48
+ // for each pending session so we share the same locking + transformation logic.
49
+ function listActiveSessions() {
50
+ if (!fs.existsSync(config_1.QUEUE_DIR))
51
+ return [];
52
+ const out = new Set();
53
+ for (const f of fs.readdirSync(config_1.QUEUE_DIR)) {
54
+ if (f.endsWith(".jsonl")) {
55
+ out.add(f.replace(/\.jsonl$/, ""));
56
+ }
57
+ else if (f.includes(".jsonl.draining.")) {
58
+ out.add(f.split(".jsonl.")[0]);
59
+ }
60
+ }
61
+ return Array.from(out);
62
+ }
63
+ // queueDepth must account for both live spool files AND orphan
64
+ // `.jsonl.draining.*` snapshots. The doctor surfaces oldestAgeSec as the
65
+ // signal that something is stuck; an interrupted flush leaves the live file
66
+ // gone and only a draining-suffixed remnant behind. If we measured age from
67
+ // .jsonl files alone, a queue with N orphan drains stranded for hours would
68
+ // show "oldest age n/a" and the operator would call doctor GREEN. Treat any
69
+ // readable file in the queue dir (live or draining) as evidence of pending
70
+ // work for the purposes of the oldest-age metric. `events` and `orphans`
71
+ // stay split so the operator can still see which side is heavier.
72
+ function queueDepth(queueDir = config_1.QUEUE_DIR) {
73
+ if (!fs.existsSync(queueDir))
74
+ return { sessions: 0, events: 0, orphans: 0, oldestAgeSec: null };
75
+ let events = 0;
76
+ let orphans = 0;
77
+ let oldestMs = Number.POSITIVE_INFINITY;
78
+ const sessions = new Set();
79
+ for (const f of fs.readdirSync(queueDir)) {
80
+ const full = path.join(queueDir, f);
81
+ if (f.endsWith(".jsonl")) {
82
+ sessions.add(f.replace(/\.jsonl$/, ""));
83
+ try {
84
+ const raw = fs.readFileSync(full, "utf8");
85
+ events += raw.split("\n").filter((l) => l.trim().length > 0).length;
86
+ const st = fs.statSync(full);
87
+ if (st.mtimeMs < oldestMs)
88
+ oldestMs = st.mtimeMs;
89
+ }
90
+ catch { }
91
+ }
92
+ else if (f.includes(".jsonl.draining.")) {
93
+ orphans += 1;
94
+ sessions.add(f.split(".jsonl.")[0]);
95
+ try {
96
+ const raw = fs.readFileSync(full, "utf8");
97
+ events += raw.split("\n").filter((l) => l.trim().length > 0).length;
98
+ const st = fs.statSync(full);
99
+ if (st.mtimeMs < oldestMs)
100
+ oldestMs = st.mtimeMs;
101
+ }
102
+ catch { }
103
+ }
104
+ }
105
+ return {
106
+ sessions: sessions.size,
107
+ events,
108
+ orphans,
109
+ oldestAgeSec: oldestMs === Number.POSITIVE_INFINITY ? null : Math.round((Date.now() - oldestMs) / 1000),
110
+ };
111
+ }
112
+ // Suffixes flush.sh / common.sh create per session, MOST-SPECIFIC FIRST.
113
+ // classifyQueueFile returns on the first endsWith match, so the compound
114
+ // `.hb.lock` / `.narration-cursor.lock` MUST precede the bare `.lock`, else
115
+ // `<sid>.hb.lock` would classify under a phantom `<sid>.hb` session. reapQueue
116
+ // ONLY removes files whose suffix is in this set, so a stray non-Meetless file
117
+ // (or a `.workspaceId.bak.*` backup) is never touched. `.jsonl.draining.*` is
118
+ // handled separately (variable `.<pid>` tail). `.off` lives in session-gate/,
119
+ // not the queue dir, so it is intentionally absent here.
120
+ const SIDECAR_SUFFIXES = [
121
+ ".hb.lock",
122
+ ".narration-cursor.lock",
123
+ ".narration-cursor",
124
+ ".hb",
125
+ ".repoPath",
126
+ ".gitBaseline",
127
+ ".workspaceId",
128
+ ".turn",
129
+ ".lock",
130
+ ];
131
+ const DEFAULT_QUEUE_GC_MAX_AGE_SEC = 86_400; // 24h idle before a session is litter
132
+ // Map a queue-dir filename to its (sessionId, kind) or null if it is not a
133
+ // recognized Meetless artifact. Draining snapshots come first because they also
134
+ // end in a numeric `.draining.<pid>` tail that the plain-suffix match would miss.
135
+ function classifyQueueFile(name) {
136
+ const drainIdx = name.indexOf(".jsonl.draining.");
137
+ if (drainIdx > 0) {
138
+ return { sessionId: name.slice(0, drainIdx), isSpool: false, isDraining: true };
139
+ }
140
+ if (name.endsWith(".jsonl")) {
141
+ return { sessionId: name.slice(0, -".jsonl".length), isSpool: true, isDraining: false };
142
+ }
143
+ for (const sfx of SIDECAR_SUFFIXES) {
144
+ if (name.endsWith(sfx)) {
145
+ return { sessionId: name.slice(0, -sfx.length), isSpool: false, isDraining: false };
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ // reapQueue: age-gated GC of dead-session litter (RC2). A session is reaped only
151
+ // when it has NO undelivered work (no non-empty `.jsonl`, no `.jsonl.draining.*`)
152
+ // AND its NEWEST file is older than maxAgeSec. Newest-mtime is the liveness
153
+ // heartbeat: an active session's `.jsonl`/`.turn` is rewritten every turn, so the
154
+ // age gate cannot reap a session that is merely between turns. Unlinking a `.lock`
155
+ // that another process holds is safe on POSIX (the lock is on the open inode; the
156
+ // holder keeps it), and the 24h gate makes a concurrent live flush impossible
157
+ // anyway. Pure function of the filesystem + injected `now` so it is deterministic
158
+ // under test.
159
+ function reapQueue(opts = {}) {
160
+ const queueDir = opts.queueDir ?? config_1.QUEUE_DIR;
161
+ const now = opts.now ?? Date.now();
162
+ const dryRun = opts.dryRun ?? false;
163
+ const maxAgeSec = opts.maxAgeSec ??
164
+ (process.env.MEETLESS_QUEUE_GC_MAX_AGE_SEC
165
+ ? Number(process.env.MEETLESS_QUEUE_GC_MAX_AGE_SEC)
166
+ : DEFAULT_QUEUE_GC_MAX_AGE_SEC);
167
+ const result = { reaped: [], removedFiles: 0, skippedPending: 0, skippedFresh: 0 };
168
+ if (!fs.existsSync(queueDir))
169
+ return result;
170
+ const groups = new Map();
171
+ for (const name of fs.readdirSync(queueDir)) {
172
+ const c = classifyQueueFile(name);
173
+ if (!c)
174
+ continue; // unrecognized -> never touch
175
+ const full = path.join(queueDir, name);
176
+ let g = groups.get(c.sessionId);
177
+ if (!g) {
178
+ g = { files: [], hasNonEmptySpool: false, hasDraining: false, newestMtimeMs: 0 };
179
+ groups.set(c.sessionId, g);
180
+ }
181
+ g.files.push(full);
182
+ let mtimeMs = 0;
183
+ let sizeNonEmpty = false;
184
+ try {
185
+ const st = fs.statSync(full);
186
+ mtimeMs = st.mtimeMs;
187
+ sizeNonEmpty = st.size > 0;
188
+ }
189
+ catch {
190
+ continue;
191
+ }
192
+ if (mtimeMs > g.newestMtimeMs)
193
+ g.newestMtimeMs = mtimeMs;
194
+ if (c.isSpool && sizeNonEmpty)
195
+ g.hasNonEmptySpool = true;
196
+ if (c.isDraining)
197
+ g.hasDraining = true;
198
+ }
199
+ for (const [sessionId, g] of groups) {
200
+ if (g.hasNonEmptySpool || g.hasDraining) {
201
+ result.skippedPending += 1;
202
+ continue;
203
+ }
204
+ const ageSec = (now - g.newestMtimeMs) / 1000;
205
+ if (ageSec < maxAgeSec) {
206
+ result.skippedFresh += 1;
207
+ continue;
208
+ }
209
+ let removed = 0;
210
+ for (const f of g.files) {
211
+ if (dryRun) {
212
+ // Count what WOULD be removed without touching disk (doctor's read-only
213
+ // debt probe). statSync already succeeded above for files we counted, so
214
+ // treat every grouped file as removable here.
215
+ removed += 1;
216
+ continue;
217
+ }
218
+ try {
219
+ fs.rmSync(f, { force: true });
220
+ removed += 1;
221
+ }
222
+ catch { }
223
+ }
224
+ if (removed > 0) {
225
+ result.reaped.push(sessionId);
226
+ result.removedFiles += removed;
227
+ }
228
+ }
229
+ return result;
230
+ }
231
+ // planQueuePrune: the EXPLICIT reclaimer (powers `mla queue prune`). Unlike
232
+ // reapQueue it INCLUDES sessions with a non-empty `.jsonl` -- the stranded tails
233
+ // reapQueue refuses by its data-loss-safety rule. Safe only because prune is
234
+ // (a) manual, (b) age-gated (a session no hook has touched in maxAgeSec is dead),
235
+ // and (c) flushed best-effort before deletion (executeQueuePrune). Pure function
236
+ // of the filesystem + injected `now`.
237
+ function planQueuePrune(opts = {}) {
238
+ const queueDir = opts.queueDir ?? config_1.QUEUE_DIR;
239
+ const now = opts.now ?? Date.now();
240
+ const maxAgeSec = opts.maxAgeSec ?? DEFAULT_QUEUE_GC_MAX_AGE_SEC;
241
+ const plan = {
242
+ candidates: [],
243
+ skippedFresh: 0,
244
+ totalFiles: 0,
245
+ totalUnflushedEvents: 0,
246
+ totalBytes: 0,
247
+ oldestAgeSec: null,
248
+ };
249
+ if (!fs.existsSync(queueDir))
250
+ return plan;
251
+ const groups = new Map();
252
+ for (const name of fs.readdirSync(queueDir)) {
253
+ const c = classifyQueueFile(name);
254
+ if (!c)
255
+ continue;
256
+ if (opts.sessionId && c.sessionId !== opts.sessionId)
257
+ continue;
258
+ const full = path.join(queueDir, name);
259
+ let g = groups.get(c.sessionId);
260
+ if (!g) {
261
+ g = { files: [], unflushedEvents: 0, bytes: 0, newestMtimeMs: 0 };
262
+ groups.set(c.sessionId, g);
263
+ }
264
+ g.files.push(full);
265
+ try {
266
+ const st = fs.statSync(full);
267
+ if (st.mtimeMs > g.newestMtimeMs)
268
+ g.newestMtimeMs = st.mtimeMs;
269
+ g.bytes += st.size;
270
+ if ((c.isSpool || c.isDraining) && st.size > 0) {
271
+ const raw = fs.readFileSync(full, "utf8");
272
+ g.unflushedEvents += raw.split("\n").filter((l) => l.trim().length > 0).length;
273
+ }
274
+ }
275
+ catch {
276
+ // unreadable -> still listed for deletion, just not counted
277
+ }
278
+ }
279
+ let oldest = null;
280
+ for (const [sessionId, g] of groups) {
281
+ const ageSec = (now - g.newestMtimeMs) / 1000;
282
+ if (ageSec < maxAgeSec) {
283
+ plan.skippedFresh += 1;
284
+ continue;
285
+ }
286
+ plan.candidates.push({
287
+ sessionId,
288
+ files: g.files,
289
+ unflushedEvents: g.unflushedEvents,
290
+ bytes: g.bytes,
291
+ newestMtimeMs: g.newestMtimeMs,
292
+ ageSec: Math.round(ageSec),
293
+ });
294
+ plan.totalFiles += g.files.length;
295
+ plan.totalUnflushedEvents += g.unflushedEvents;
296
+ plan.totalBytes += g.bytes;
297
+ if (oldest === null || ageSec > oldest)
298
+ oldest = ageSec;
299
+ }
300
+ plan.candidates.sort((a, b) => b.ageSec - a.ageSec);
301
+ plan.oldestAgeSec = oldest === null ? null : Math.round(oldest);
302
+ return plan;
303
+ }
304
+ // executeQueuePrune: best-effort flush each candidate (so deliverable events are
305
+ // recovered, not discarded), then delete its remaining files. Flush is via the
306
+ // real flush.sh (shared locking + transform), failures ignored -- a 3-day-dead
307
+ // session whose AgentRun is gone simply will not deliver, and we prune it anyway.
308
+ function executeQueuePrune(plan, opts = {}) {
309
+ const hookDir = opts.hookDir ?? config_1.HOOKS_DIR;
310
+ const doFlush = opts.flush ?? true;
311
+ const result = {
312
+ prunedSessions: [],
313
+ removedFiles: 0,
314
+ discardedEvents: 0,
315
+ flushedSessions: 0,
316
+ recoveredFiles: 0,
317
+ };
318
+ for (const c of plan.candidates) {
319
+ if (doFlush && c.unflushedEvents > 0 && fs.existsSync(path.join(hookDir, "flush.sh"))) {
320
+ const r = runFlushScript(c.sessionId, hookDir);
321
+ if (r.ok)
322
+ result.flushedSessions += 1;
323
+ }
324
+ let removed = 0;
325
+ let discarded = 0;
326
+ for (const f of c.files) {
327
+ try {
328
+ const st = fs.statSync(f); // throws if a flush already removed it
329
+ if ((f.endsWith(".jsonl") || f.includes(".jsonl.draining.")) && st.size > 0) {
330
+ const raw = fs.readFileSync(f, "utf8");
331
+ discarded += raw.split("\n").filter((l) => l.trim().length > 0).length;
332
+ }
333
+ fs.rmSync(f, { force: true });
334
+ removed += 1;
335
+ }
336
+ catch {
337
+ result.recoveredFiles += 1; // gone already (flush self-cleaned it)
338
+ }
339
+ }
340
+ if (removed > 0) {
341
+ result.prunedSessions.push(c.sessionId);
342
+ result.removedFiles += removed;
343
+ result.discardedEvents += discarded;
344
+ }
345
+ }
346
+ return result;
347
+ }
348
+ function runFlushScript(sessionId, hookDir) {
349
+ const flush = path.join(hookDir, "flush.sh");
350
+ if (!fs.existsSync(flush)) {
351
+ return { ok: false, stderr: `flush.sh not found at ${flush}` };
352
+ }
353
+ const r = (0, child_process_1.spawnSync)("bash", [flush, sessionId], { encoding: "utf8" });
354
+ return { ok: r.status === 0, stderr: r.stderr || "" };
355
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.makeMcpStaleCheck = makeMcpStaleCheck;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ // build-info.json lives at the dist ROOT; this module compiles to dist/lib/, so
40
+ // its sibling is one level up. Mirrors observability.ts's path convention, but
41
+ // deliberately does NOT reuse loadBuildInfo(): that one caches process-lifetime
42
+ // and synthesizes a fresh builtAt when the file is missing, both of which would
43
+ // defeat a LIVE staleness probe (a cached spawn value can never differ; a
44
+ // synthetic builtAt would false-positive on a real dev build).
45
+ const DEFAULT_BUILD_INFO_PATH = path.join(__dirname, "..", "build-info.json");
46
+ function defaultReadBuildIdentity() {
47
+ try {
48
+ const raw = fs.readFileSync(DEFAULT_BUILD_INFO_PATH, "utf8");
49
+ const parsed = JSON.parse(raw);
50
+ if (typeof parsed.builtAt !== "string" || typeof parsed.sha !== "string") {
51
+ return null;
52
+ }
53
+ return { sha: parsed.sha, builtAt: parsed.builtAt };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Build the per-call staleness probe for a long-lived `mla mcp` server. Snapshots
61
+ * the build identity at spawn, then on each call re-reads it and returns a one-
62
+ * line warning when a newer build has landed on disk, else null. Never throws,
63
+ * always fails open: if there was no identity at spawn (dev build), or either
64
+ * read is unavailable, it stays silent.
65
+ */
66
+ function makeMcpStaleCheck(deps = {}) {
67
+ const read = deps.readBuildIdentity ?? defaultReadBuildIdentity;
68
+ let spawn;
69
+ try {
70
+ spawn = read();
71
+ }
72
+ catch {
73
+ spawn = null;
74
+ }
75
+ return () => {
76
+ // No spawn baseline (dev build) means we cannot tell "rebuilt" from "first
77
+ // ever stamp", so we never nag.
78
+ if (!spawn)
79
+ return null;
80
+ let current;
81
+ try {
82
+ current = read();
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ if (!current)
88
+ return null;
89
+ if (current.builtAt === spawn.builtAt && current.sha === spawn.sha) {
90
+ return null;
91
+ }
92
+ return staleWarning(spawn, current);
93
+ };
94
+ }
95
+ function staleWarning(spawn, current) {
96
+ return (`Meetless MCP is serving an OLDER build (${spawn.sha} @ ${spawn.builtAt}); ` +
97
+ `a newer mla build (${current.sha} @ ${current.builtAt}) is now on disk. ` +
98
+ `Restart your editor so the MCP server reloads; until then every result, ` +
99
+ `including this one, comes from the older code.`);
100
+ }