@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,221 @@
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.parseArgs = parseArgs;
37
+ exports.triggerSessionFinalize = triggerSessionFinalize;
38
+ exports.runInternalFinalize = runInternalFinalize;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const config_1 = require("../lib/config");
42
+ const workspace_1 = require("../lib/workspace");
43
+ const http_1 = require("../lib/http");
44
+ const git_1 = require("../lib/git");
45
+ // `mla _internal finalize-session <sessionId>` (§5.2 + Correction 6)
46
+ //
47
+ // Invoked by flush.sh after it drains a session and observes a
48
+ // finalize_requested event. CLI is responsible for git evidence capture
49
+ // because hooks must NOT shell out to git (slow + sandbox unpredictability).
50
+ //
51
+ // Wire: POST /internal/v1/agent-runs/by-session/<sid>/finalize
52
+ // body: { workspaceId, gitEvidence } (Correction 6: no finalMessage)
53
+ //
54
+ // Git is opportunistic corroboration, NOT the source of truth (Decision 7,
55
+ // note 20260528 §11). The source of truth for what a session changed is the
56
+ // coding agent's own text report (captured by stop.sh into
57
+ // session_stopped.finalMessage -> agentClaimsRaw). Meetless is a coordination
58
+ // layer, not a git forensics tool; we do not reconstruct "what changed" from
59
+ // disk state. Git evidence is captured ONLY as supplementary "actuals" when the
60
+ // session cwd resolves to a single repo.
61
+ //
62
+ // We prefer $MEETLESS_REPO_PATH (set by flush.sh from the session_started
63
+ // repoPath sidecar) over process.cwd(). When that path is a real repo,
64
+ // captureGitEvidence returns the actuals (branch, last commit, changed files,
65
+ // diff stat). When it is a non-repo (e.g. a parent folder holding many child
66
+ // repos, or a scratch dir), captureGitEvidence returns an empty shell with the
67
+ // reason in `errors[]` (`{topLevel: "", errors: ["toplevel:fatal: not a git
68
+ // repository"], ...}`). Either way we POST finalize: the worker degrades
69
+ // gracefully on empty git, and `errors[]` keeps the absence visible rather than
70
+ // silently dropped (preserving Epoch 33's anti-silent-loss intent without the
71
+ // hard block).
72
+ //
73
+ // Why no block: a non-repo cwd is a legitimate, supported setup, not a wrong-cwd
74
+ // retry to fix. The earlier guard (Epoch 33) refused to POST on empty topLevel
75
+ // and re-spooled finalize_requested, which re-spooled FOREVER in a multi-repo
76
+ // parent and wedged the whole review loop. Git absence is now just absence.
77
+ // Strict argv parsing for `mla _internal finalize-session` (Wedge v6
78
+ // Epoch 53). The internal subcommand is fired by flush.sh on every
79
+ // drained session's finalize_requested event:
80
+ //
81
+ // "$MLA_PATH" _internal finalize-session "$SESSION_ID"
82
+ //
83
+ // flush.sh always passes exactly one positional. The CLI's old guard
84
+ // was `argv.length < 1` which masked three silent drops if the hook
85
+ // is ever invoked with anything other than that exact shape:
86
+ //
87
+ // 1. `mla _internal finalize-session sid extra` silently dropped
88
+ // "extra". An accidental flush.sh refactor that appended a
89
+ // second positional would silently target one sessionId but
90
+ // look like it accepted two.
91
+ //
92
+ // 2. `mla _internal finalize-session --foo` bound sessionId="--foo"
93
+ // and the server then 404'd opaquely. A flush.sh template bug
94
+ // that emitted a flag in the SESSION_ID slot (e.g. shell glob
95
+ // expansion gone wrong) would silently 404.
96
+ //
97
+ // 3. Zero positionals returned exit 2 with the usage line, which
98
+ // is correct -- but the same path is preserved here through
99
+ // the throw + catch convention so it stays symmetric with the
100
+ // other strict parsers.
101
+ //
102
+ // Strict rules below:
103
+ // - Exactly one positional (sessionId). Zero or two+ throw.
104
+ // - Zero flags supported. Any `--`-prefixed or `-`-prefixed token
105
+ // throws.
106
+ function parseArgs(argv) {
107
+ let sessionId;
108
+ for (const a of argv) {
109
+ if (a.startsWith("--") || a.startsWith("-")) {
110
+ throw new Error(`Unknown flag: ${a}. \`mla _internal finalize-session\` takes no flags, only <sessionId>.`);
111
+ }
112
+ if (sessionId !== undefined) {
113
+ throw new Error(`Unexpected extra positional argument: ${a}. \`mla _internal finalize-session\` takes exactly one sessionId.`);
114
+ }
115
+ sessionId = a;
116
+ }
117
+ if (sessionId === undefined) {
118
+ throw new Error("usage: mla _internal finalize-session <sessionId>");
119
+ }
120
+ return { sessionId };
121
+ }
122
+ // The finalize core, extracted so it can be fired by more than the Stop hook.
123
+ //
124
+ // Two callers share it:
125
+ // 1. `mla _internal finalize-session` (this file's runInternalFinalize),
126
+ // driven by flush.sh on the Stop hook's finalize_requested event.
127
+ // 2. `mla review` (Phase 7 / PATCH 5 / INV-M6,
128
+ // notes/20260604-mla-mission-and-review-packet-rethink.md), which fires
129
+ // this on demand so a review snapshot is produced WITHOUT waiting for a
130
+ // Stop signal that, in practice, never cleanly arrives. The Stop-hook
131
+ // finalize is now one trigger among several, not the sole producer.
132
+ //
133
+ // Captures git evidence (opportunistic corroboration, never the source of truth)
134
+ // and POSTs the finalize. Returns both the evidence and the resolved repoPath so
135
+ // the caller can tailor its own logging. Idempotent on runId server-side, so it
136
+ // is safe to re-fire on every `mla review` (the rolling-snapshot model). Carries
137
+ // no mission (PR1) and tolerates branch = null (INV-M1): a detached-HEAD / non-
138
+ // repo checkout yields an empty branch and the finalize still proceeds.
139
+ async function triggerSessionFinalize(sessionId, cfg) {
140
+ // Repo resolution ladder, mirroring flush.sh so both finalize callers agree:
141
+ // 1. $MEETLESS_REPO_PATH -- the Stop-hook path: flush.sh exports it from the
142
+ // <sid>.repoPath sidecar before invoking `mla _internal finalize-session`.
143
+ // 2. <sid>.repoPath sidecar -- the on-demand `mla review` path (Phase 7 /
144
+ // INV-M6) has no flush.sh wrapper to set the env var, so it reads the
145
+ // sidecar directly. Without this rung, on-demand finalize captured git
146
+ // evidence from whatever directory the human happened to type `mla review`
147
+ // in, and the rolling-snapshot finalize OVERWROTE the run with wrong-repo
148
+ // (or empty) evidence. Since Phase 7 assumes a clean Stop never arrives,
149
+ // that on-demand capture may be the only git evidence the run ever gets.
150
+ // 3. process.cwd() -- legacy fallback for a session with no sidecar (started
151
+ // before session-start.sh wrote one), preserving the original behavior.
152
+ const envRepoPath = process.env.MEETLESS_REPO_PATH;
153
+ let repoPath;
154
+ if (envRepoPath && envRepoPath.length > 0) {
155
+ repoPath = envRepoPath;
156
+ }
157
+ else {
158
+ let sidecarRepoPath = null;
159
+ try {
160
+ const raw = fs.readFileSync(path.join(config_1.QUEUE_DIR, `${sessionId}.repoPath`), "utf8").trim();
161
+ sidecarRepoPath = raw.length > 0 ? raw : null;
162
+ }
163
+ catch {
164
+ sidecarRepoPath = null;
165
+ }
166
+ repoPath = sidecarRepoPath ?? process.cwd();
167
+ }
168
+ // Folder = workspace (T1.1): the run belongs to the workspace bound to the
169
+ // SESSION REPO, resolved from the nearest `.meetless.json` marker walking up
170
+ // from repoPath -- NOT from process.cwd(). The Stop-hook path runs this via
171
+ // flush.sh, which is nohup-spawned and whose cwd is usually $HOME, so cwd is
172
+ // not the repo; repoPath (env -> sidecar -> cwd ladder above) is. Both finalize
173
+ // callers therefore agree on the workspace the same way they agree on git
174
+ // evidence: through the one resolved repo path. An unactivated repo (no marker)
175
+ // throws NotActivatedError here -- finalize cannot name a workspace to the
176
+ // server, so it correctly does not POST a half-bound run.
177
+ const workspaceId = (0, workspace_1.resolveWorkspaceId)(repoPath);
178
+ // Read the session-start git baseline sidecar (written by session-start.sh).
179
+ // Subtracting it makes captureGitEvidence attribute only session-touched
180
+ // changes, not ambient dirty state the tree carried before the agent ran.
181
+ // Absent sidecar (older session, or session-start hook never ran) => null =>
182
+ // original whole-tree behavior, so this is fully backward-compatible.
183
+ const baselinePath = path.join(config_1.QUEUE_DIR, `${sessionId}.gitBaseline`);
184
+ let baseline = null;
185
+ try {
186
+ baseline = fs.readFileSync(baselinePath, "utf8");
187
+ }
188
+ catch {
189
+ baseline = null;
190
+ }
191
+ const git = (0, git_1.captureGitEvidence)(repoPath, baseline);
192
+ await (0, http_1.post)(cfg, `/internal/v1/agent-runs/by-session/${encodeURIComponent(sessionId)}/finalize`, {
193
+ workspaceId,
194
+ gitEvidence: git,
195
+ }, 15000);
196
+ return { git, repoPath, workspaceId };
197
+ }
198
+ async function runInternalFinalize(argv) {
199
+ let sessionId;
200
+ try {
201
+ ({ sessionId } = parseArgs(argv));
202
+ }
203
+ catch (e) {
204
+ console.error(e.message);
205
+ return 2;
206
+ }
207
+ // Folder = workspace (T1.1): credentials only here. The run's workspaceId is
208
+ // resolved INSIDE triggerSessionFinalize from the `.meetless.json` marker
209
+ // at/above the resolved session repo path (NOT process.cwd()), because flush.sh
210
+ // is nohup-spawned and runs this from $HOME -- cwd is not the repo.
211
+ const cfg = (0, config_1.readConfig)();
212
+ const { git, repoPath } = await triggerSessionFinalize(sessionId, cfg);
213
+ if (!git.topLevel) {
214
+ console.log(`Finalize accepted for session ${sessionId} (no git corroboration: ${repoPath} is not a single repo; ` +
215
+ `agent report remains the source of truth).`);
216
+ }
217
+ else {
218
+ console.log(`Finalize accepted for session ${sessionId}.`);
219
+ }
220
+ return 0;
221
+ }
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ // `mla _internal pretool-observe` (A1: make the R1 notes-location pilot live). The managed
3
+ // pre-tool-use.sh hook pipes its raw PreToolUse stdin into this subcommand and forwards whatever it
4
+ // writes on stdout back to Claude Code as the hook response.
5
+ //
6
+ // In R0 this subcommand was decision-free by construction (a constant `{}`). A1 wires it to the
7
+ // version-backed enforce seam (`evaluateAndEnforceNotesVersion`): it now emits a real deny on the wire
8
+ // when, and ONLY when, a human-attested LIVE version is VIOLATED and the deny is admitted by the
9
+ // deny-admission gates (P0.58 sole input authority, P0.63 attested path root). Everything else is the
10
+ // empty pass-through. Two invariants are load-bearing:
11
+ //
12
+ // 1. The decision is COMPUTED from the rule evaluation, never reflected from the input. A payload that
13
+ // smuggles a `hookSpecificOutput.permissionDecision` cannot survive into stdout: the only deny this
14
+ // command can emit is the one the seam returns against the one human-attested LIVE version, with a
15
+ // reason the seam builds (`buildDenyReason`), never the attacker's string.
16
+ //
17
+ // 2. The hook can never block on infrastructure. A PreToolUse exit 2 (or a thrown error, or a missing
18
+ // `mla`) would block the tool; we must never do that. Every failure path -- unreadable stdin,
19
+ // malformed payload, store open failure, a throwing dependency -- fails OPEN to the exit-0
20
+ // pass-through. The decision travels in the JSON body, never the exit code.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.PRETOOL_PASS_THROUGH = void 0;
23
+ exports.renderPreToolUseResponse = renderPreToolUseResponse;
24
+ exports.renderConflictWarning = renderConflictWarning;
25
+ exports.runInternalPretoolObserve = runInternalPretoolObserve;
26
+ const os_1 = require("os");
27
+ const active_conflict_cache_1 = require("../lib/active-conflict-cache");
28
+ const ce0_store_1 = require("../lib/rules/ce0-store");
29
+ const enforce_notes_version_1 = require("../lib/rules/enforce-notes-version");
30
+ const observe_adapter_1 = require("../lib/rules/observe-adapter");
31
+ const runtime_scope_1 = require("../lib/rules/runtime-scope");
32
+ const live_input_authority_1 = require("../lib/rules/live-input-authority");
33
+ const cache_1 = require("../lib/scanner/cache");
34
+ const workspace_1 = require("../lib/workspace");
35
+ // The empty pass-through response: exit-0, carries no `hookSpecificOutput`, hence grants nothing and
36
+ // decides nothing (the documented Claude Code no-decision body).
37
+ exports.PRETOOL_PASS_THROUGH = {};
38
+ /**
39
+ * Pure. Map the enforce seam's internal response to the Claude Code PreToolUse wire shape. A deny
40
+ * becomes the documented `hookSpecificOutput` deny body; everything else is the empty pass-through.
41
+ * Exit code is ALWAYS 0: the decision rides the body, never the exit code (an exit 2 would block).
42
+ */
43
+ function renderPreToolUseResponse(seam) {
44
+ if ("permissionDecision" in seam && seam.permissionDecision === "deny") {
45
+ return {
46
+ stdout: JSON.stringify({
47
+ hookSpecificOutput: {
48
+ hookEventName: "PreToolUse",
49
+ permissionDecision: "deny",
50
+ permissionDecisionReason: seam.reason,
51
+ },
52
+ }),
53
+ exitCode: 0,
54
+ };
55
+ }
56
+ return { stdout: JSON.stringify(exports.PRETOOL_PASS_THROUGH), exitCode: 0 };
57
+ }
58
+ /**
59
+ * Pure. Build the SOFT cross-session conflict warning (G8 / D1 §11.3, CRITICAL-5)
60
+ * for the session's open conflicts. It carries NO `permissionDecision`, so the tool
61
+ * is PERMITTED: Claude Code falls through to its normal permission flow. The body
62
+ * splits the two audiences the way Claude Code routes them:
63
+ * - `systemMessage` is shown to the human operator (a one-line "heads up, an open
64
+ * conflict touches this work, resolve it in /inbox").
65
+ * - `hookSpecificOutput.additionalContext` is fed to the agent so it can pause the
66
+ * conflicting line of work on its own.
67
+ * The hard default-deny is DEFERRED (§0.1): a fail-closed gate on a possibly-stale
68
+ * snapshot would brick coding sessions, so this surface only ever warns. The mode is
69
+ * named in the agent context purely for transparency; it never gates the wire shape
70
+ * here (both soft and hard render the same warning until the deny path is built).
71
+ */
72
+ function renderConflictWarning(conflicts, mode) {
73
+ const first = conflicts[0];
74
+ const extra = conflicts.length - 1;
75
+ const more = extra > 0 ? ` (and ${extra} more open on this session)` : "";
76
+ const systemMessage = `Meetless: a pending cross-session conflict touches this work. ` +
77
+ `${first.reason} Case ${first.caseId}${more}. ` +
78
+ `Resolve it in /inbox before relying on this change.`;
79
+ const additionalContext = `Meetless D1 early warning (gate: ${mode}). ${conflicts.length} open ` +
80
+ `cross-session conflict(s) on this session. ${first.reason} ` +
81
+ `Case ${first.caseId} opened ${first.openedAt}${more}. ` +
82
+ `This is advisory and the tool is permitted; pause this line of work and ` +
83
+ `check /inbox for the human decision before continuing.`;
84
+ return {
85
+ stdout: JSON.stringify({
86
+ systemMessage,
87
+ hookSpecificOutput: {
88
+ hookEventName: "PreToolUse",
89
+ additionalContext,
90
+ },
91
+ }),
92
+ exitCode: 0,
93
+ };
94
+ }
95
+ function readStdinReal() {
96
+ return new Promise((resolve, reject) => {
97
+ const chunks = [];
98
+ process.stdin.on("data", (c) => chunks.push(c));
99
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
100
+ process.stdin.on("error", reject);
101
+ });
102
+ }
103
+ const defaultDeps = {
104
+ readStdin: readStdinReal,
105
+ writeOut: (s) => process.stdout.write(s),
106
+ };
107
+ /**
108
+ * Compute the PreToolUse decision for one raw stdin payload by running the enforce seam against the
109
+ * active scope's LIVE attested version. Fails OPEN to the pass-through on ANY error (store open failure,
110
+ * a throwing dependency, a seam fault) so the hook can never block a tool on infrastructure. The store
111
+ * is opened here and closed in `finally` unless the caller injected one (then the caller owns its
112
+ * lifecycle).
113
+ */
114
+ async function computePretoolDecision(raw, deps) {
115
+ const ownsStore = deps.store === undefined;
116
+ let store;
117
+ try {
118
+ store = deps.store ?? (0, ce0_store_1.openCe0Store)((0, ce0_store_1.defaultCe0StorePath)());
119
+ const scope = (deps.resolveScope ?? defaultResolveScope)();
120
+ const clock = (deps.clock ?? defaultClock)();
121
+ const resolveInputAuthority = deps.resolveInputAuthority ?? live_input_authority_1.resolveLiveInputAuthority;
122
+ const directives = (deps.resolveDirectives ?? defaultResolveDirectives)();
123
+ const { response } = await (0, enforce_notes_version_1.evaluateEnforceOrObserveNotesRule)(store, {
124
+ rawStdin: raw,
125
+ runtimeProjectRoot: scope.runtimeProjectRoot,
126
+ runtimeScopeId: scope.runtimeScopeId,
127
+ createdAt: clock.createdAt,
128
+ now: clock.now,
129
+ classifyRuntime: deps.classifyRuntime,
130
+ resolveInputAuthority,
131
+ directives,
132
+ });
133
+ // The notes-version governance deny is a HARD, human-attested block and takes
134
+ // precedence: never soften a real deny into a warning. Only when the notes path
135
+ // passes through do we layer the SOFT cross-session conflict warning (which can
136
+ // only ever advise, never deny).
137
+ if ("permissionDecision" in response && response.permissionDecision === "deny") {
138
+ return renderPreToolUseResponse(response);
139
+ }
140
+ return computeConflictWarning(raw, deps);
141
+ }
142
+ catch {
143
+ // No failure may escalate into a blocking hook decision.
144
+ return renderPreToolUseResponse(exports.PRETOOL_PASS_THROUGH);
145
+ }
146
+ finally {
147
+ if (ownsStore && store) {
148
+ try {
149
+ (0, ce0_store_1.closeCe0Store)(store);
150
+ }
151
+ catch {
152
+ // Closing the store must never affect the (already computed) decision.
153
+ }
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * The SOFT cross-session conflict warning layer. Runs only after the notes path
159
+ * passes through (a notes deny already returned). Parses the session id from the raw
160
+ * stdin, reads the session's zero-network open-conflict snapshot, and warns when one
161
+ * is open. Fails OPEN to the pass-through on a missing session id, an empty or stale
162
+ * snapshot, or ANY error: the warning is advisory, so silence is always the safe
163
+ * direction. Synchronous (the snapshot read is a local file read, no network).
164
+ *
165
+ * The matcher pins this hook to Write|Edit, so the warning fires immediately before a
166
+ * file mutation, the highest-value moment to interrupt work built on a contested
167
+ * assumption. The turn-level open-time steer (§11.1) is the complementary channel that
168
+ * reaches the agent regardless of which tool runs; this decision function is itself
169
+ * tool-agnostic, so broadening the matcher later needs no change here.
170
+ */
171
+ function computeConflictWarning(raw, deps) {
172
+ try {
173
+ const sessionId = (0, observe_adapter_1.parsePreToolUseInput)(raw)?.session_id;
174
+ if (!sessionId)
175
+ return renderPreToolUseResponse(exports.PRETOOL_PASS_THROUGH);
176
+ const readConflicts = deps.readConflicts ?? ((sid) => (0, active_conflict_cache_1.readActiveConflicts)(sid));
177
+ const conflicts = readConflicts(sessionId);
178
+ if (conflicts.length === 0)
179
+ return renderPreToolUseResponse(exports.PRETOOL_PASS_THROUGH);
180
+ const mode = (deps.resolveGateMode ?? active_conflict_cache_1.resolveConflictGateMode)();
181
+ return renderConflictWarning(conflicts, mode);
182
+ }
183
+ catch {
184
+ // The conflict warning is advisory; any fault degrades to no warning, never a block.
185
+ return renderPreToolUseResponse(exports.PRETOOL_PASS_THROUGH);
186
+ }
187
+ }
188
+ function defaultResolveScope() {
189
+ const root = (0, runtime_scope_1.resolveActiveRuntimeScopeId)();
190
+ return { runtimeScopeId: root, runtimeProjectRoot: root };
191
+ }
192
+ function defaultClock() {
193
+ const now = Date.now();
194
+ return { now, createdAt: new Date(now).toISOString() };
195
+ }
196
+ /** Read the active workspace's scanned directives from the scan cache (the source of the pilot rule
197
+ * for the R0 observe substrate). Fails OPEN to none on any error or a missing cache: an unreadable
198
+ * scan cache must degrade to "no rule observed", never block the hook. */
199
+ function defaultResolveDirectives() {
200
+ try {
201
+ const workspaceId = (0, workspace_1.resolveWorkspaceIdWithEnv)();
202
+ if (!workspaceId)
203
+ return [];
204
+ const cache = (0, cache_1.readScanCache)((0, os_1.homedir)(), workspaceId);
205
+ return cache?.directives ?? [];
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ }
211
+ // IO shell. Reads stdin best-effort (a read failure still yields the pass-through body so the hook never
212
+ // blocks a tool), computes the decision, writes stdout, returns the exit code (always 0). Takes no argv.
213
+ async function runInternalPretoolObserve(_argv, deps = defaultDeps) {
214
+ let raw = "";
215
+ try {
216
+ raw = await deps.readStdin();
217
+ }
218
+ catch {
219
+ // A stdin read error must never escalate into a blocking hook decision.
220
+ raw = "";
221
+ }
222
+ const out = await computePretoolDecision(raw, deps);
223
+ deps.writeOut(out.stdout);
224
+ return out.exitCode;
225
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRefreshArgs = parseRefreshArgs;
4
+ exports.runInternalRefresh = runInternalRefresh;
5
+ const config_1 = require("../lib/config");
6
+ const http_1 = require("../lib/http");
7
+ // `mla _internal refresh [--quiet] [--if-expiring-within <secs>]` (Part 3, T2).
8
+ //
9
+ // A thin policy wrapper over the existing concurrency-safe `refreshUserToken`
10
+ // (lib/http.ts). It reimplements NOTHING: it reads the config, guards the mode,
11
+ // optionally short-circuits on a comfortably-fresh token, then calls
12
+ // refreshUserToken and maps its RefreshOutcome to a sysexits process exit code
13
+ // the bash hooks branch on with a clean `case "$rc"`.
14
+ //
15
+ // Wire contract (the 75/77/64 numbers are hardcoded in common.sh; keep in sync):
16
+ // refreshed -> 0 token rotated, adopted from a concurrent winner, or the
17
+ // `--if-expiring-within` gate found it comfortably fresh.
18
+ // busy -> 75 EX_TEMPFAIL: sidecar lock contended past cap, or transient
19
+ // outage. Session untouched; the hook keeps events queued.
20
+ // expired -> 77 EX_NOPERM: the refresh token itself is dead server-side.
21
+ // The hook surfaces enrichment_unauthorized + "run `mla login`".
22
+ // wrong mode-> 64 EX_USAGE: shared-key / none / unreadable config / bad args.
23
+ // Refresh is meaningless; the hook surfaces a mode message.
24
+ //
25
+ // SECURITY: this command NEVER prints a token. It prints at most a one-line
26
+ // status to stdout (suppressed by --quiet) and a one-line error to stderr.
27
+ const EX_OK = 0;
28
+ const EX_USAGE = 64; // EX_USAGE: wrong mode, unreadable config, or bad args.
29
+ const EX_TEMPFAIL = 75; // EX_TEMPFAIL: busy/transient; retry later.
30
+ const EX_NOPERM = 77; // EX_NOPERM: refresh token dead; re-login required.
31
+ const VALUE_FLAGS = new Set(["--if-expiring-within"]);
32
+ const BOOLEAN_FLAGS = new Set(["--quiet"]);
33
+ // Strict argv parsing, mirroring `mla login`'s VALUE_FLAGS/BOOLEAN_FLAGS shape.
34
+ // `mla _internal refresh` takes no positionals. Throws on any unknown flag,
35
+ // stray positional, missing value, or non-integer --if-expiring-within.
36
+ function parseRefreshArgs(argv) {
37
+ const out = { quiet: false };
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i];
40
+ if (VALUE_FLAGS.has(a)) {
41
+ const v = argv[i + 1];
42
+ if (v === undefined || v.startsWith("--") || v.startsWith("-")) {
43
+ throw new Error(`Missing value for ${a}`);
44
+ }
45
+ if (a === "--if-expiring-within") {
46
+ const secs = Number(v);
47
+ if (!Number.isInteger(secs) || secs < 0) {
48
+ throw new Error(`Invalid --if-expiring-within value "${v}": expected a non-negative integer (seconds).`);
49
+ }
50
+ out.ifExpiringWithinSecs = secs;
51
+ }
52
+ i += 1;
53
+ continue;
54
+ }
55
+ if (BOOLEAN_FLAGS.has(a)) {
56
+ if (a === "--quiet")
57
+ out.quiet = true;
58
+ continue;
59
+ }
60
+ if (a.startsWith("--") || a.startsWith("-")) {
61
+ throw new Error(`Unknown flag: ${a}. Supported flags: ${[...VALUE_FLAGS, ...BOOLEAN_FLAGS].sort().join(", ")}`);
62
+ }
63
+ throw new Error(`Unexpected positional argument: ${a}. \`mla _internal refresh\` takes no positionals.`);
64
+ }
65
+ return out;
66
+ }
67
+ async function runInternalRefresh(argv, deps = {}) {
68
+ const refresh = deps.refresh ?? http_1.refreshUserToken;
69
+ const now = deps.now ?? (() => Date.now());
70
+ let flags;
71
+ try {
72
+ flags = parseRefreshArgs(argv);
73
+ }
74
+ catch (e) {
75
+ console.error(e.message);
76
+ return EX_USAGE;
77
+ }
78
+ // Read config. readConfig throws loudly on a missing/corrupt config or the
79
+ // Gate-4 env conflict (user-token on disk + MEETLESS_CONTROL_TOKEN). Any of
80
+ // these means we cannot name a refreshable user session: surface and bail 64.
81
+ let cfg;
82
+ try {
83
+ cfg = (0, config_1.readConfig)();
84
+ }
85
+ catch (e) {
86
+ console.error(e.message);
87
+ return EX_USAGE;
88
+ }
89
+ // Mode guard BEFORE refreshUserToken so we can distinguish "wrong mode" (64)
90
+ // from "dead refresh token" (77). shared-key and none have no refresh token.
91
+ if (cfg.auth.mode !== "user-token") {
92
+ if (cfg.auth.mode === "shared-key") {
93
+ console.error("This is a shared-key session (no refresh token to rotate). " +
94
+ "Re-key with `mla init --control-token <T>` if it is invalid.");
95
+ }
96
+ else {
97
+ console.error("Not logged in. Run `mla login` first.");
98
+ }
99
+ return EX_USAGE;
100
+ }
101
+ // Proactive gate (Part 3 §A): if --if-expiring-within is set and the access
102
+ // token has MORE than that much runway, no-op with no network call. An
103
+ // unparseable expiry parses to NaN; we treat that as "cannot prove fresh" and
104
+ // fall through to a real refresh rather than trust a broken timestamp (same
105
+ // NaN-safe philosophy as `mla login`).
106
+ if (flags.ifExpiringWithinSecs !== undefined) {
107
+ const remainingMs = Date.parse(cfg.auth.accessExpiresAt) - now();
108
+ const thresholdMs = flags.ifExpiringWithinSecs * 1000;
109
+ if (!Number.isNaN(remainingMs) && remainingMs > thresholdMs) {
110
+ if (!flags.quiet) {
111
+ console.log("Access token still fresh; no refresh needed.");
112
+ }
113
+ return EX_OK;
114
+ }
115
+ }
116
+ const outcome = await refresh(cfg);
117
+ switch (outcome) {
118
+ case "refreshed":
119
+ if (!flags.quiet)
120
+ console.log("Access token refreshed.");
121
+ return EX_OK;
122
+ case "busy":
123
+ console.error("Refresh busy (lock contended or transient outage); will retry later.");
124
+ return EX_TEMPFAIL;
125
+ case "expired":
126
+ console.error("Session expired server-side. Run `mla login`.");
127
+ return EX_NOPERM;
128
+ default: {
129
+ // Exhaustiveness guard: a new RefreshOutcome must be mapped explicitly
130
+ // rather than silently treated as success.
131
+ const _never = outcome;
132
+ console.error(`Unexpected refresh outcome: ${String(_never)}`);
133
+ return EX_TEMPFAIL;
134
+ }
135
+ }
136
+ }