@plebeiantech/lat.md 0.12.3-fork.1

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/src/cli/check-frontmatter.d.ts +12 -0
  4. package/dist/src/cli/check-frontmatter.js +44 -0
  5. package/dist/src/cli/check-mode.d.ts +6 -0
  6. package/dist/src/cli/check-mode.js +303 -0
  7. package/dist/src/cli/check-status.d.ts +74 -0
  8. package/dist/src/cli/check-status.js +217 -0
  9. package/dist/src/cli/check.d.ts +45 -0
  10. package/dist/src/cli/check.js +777 -0
  11. package/dist/src/cli/checklist-menu.d.ts +12 -0
  12. package/dist/src/cli/checklist-menu.js +111 -0
  13. package/dist/src/cli/comment-guard.d.ts +16 -0
  14. package/dist/src/cli/comment-guard.js +111 -0
  15. package/dist/src/cli/comment-reminder.d.ts +62 -0
  16. package/dist/src/cli/comment-reminder.js +210 -0
  17. package/dist/src/cli/context.d.ts +11 -0
  18. package/dist/src/cli/context.js +58 -0
  19. package/dist/src/cli/expand.d.ts +8 -0
  20. package/dist/src/cli/expand.js +94 -0
  21. package/dist/src/cli/gen-index.d.ts +45 -0
  22. package/dist/src/cli/gen-index.js +235 -0
  23. package/dist/src/cli/gen.d.ts +6 -0
  24. package/dist/src/cli/gen.js +42 -0
  25. package/dist/src/cli/graph.d.ts +7 -0
  26. package/dist/src/cli/graph.js +47 -0
  27. package/dist/src/cli/hook.d.ts +1 -0
  28. package/dist/src/cli/hook.js +432 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +307 -0
  31. package/dist/src/cli/init.d.ts +9 -0
  32. package/dist/src/cli/init.js +1031 -0
  33. package/dist/src/cli/link-scheme.d.ts +54 -0
  34. package/dist/src/cli/link-scheme.js +101 -0
  35. package/dist/src/cli/locate.d.ts +2 -0
  36. package/dist/src/cli/locate.js +17 -0
  37. package/dist/src/cli/refs.d.ts +22 -0
  38. package/dist/src/cli/refs.js +229 -0
  39. package/dist/src/cli/reindex.d.ts +13 -0
  40. package/dist/src/cli/reindex.js +136 -0
  41. package/dist/src/cli/search.d.ts +36 -0
  42. package/dist/src/cli/search.js +217 -0
  43. package/dist/src/cli/section.d.ts +43 -0
  44. package/dist/src/cli/section.js +252 -0
  45. package/dist/src/cli/select-menu.d.ts +13 -0
  46. package/dist/src/cli/select-menu.js +100 -0
  47. package/dist/src/cli/templates.d.ts +1 -0
  48. package/dist/src/cli/templates.js +15 -0
  49. package/dist/src/cli/ui.d.ts +11 -0
  50. package/dist/src/cli/ui.js +34 -0
  51. package/dist/src/code-refs.d.ts +17 -0
  52. package/dist/src/code-refs.js +277 -0
  53. package/dist/src/config.d.ts +29 -0
  54. package/dist/src/config.js +83 -0
  55. package/dist/src/context.d.ts +23 -0
  56. package/dist/src/context.js +11 -0
  57. package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
  58. package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
  59. package/dist/src/extensions/wiki-link/index.d.ts +4 -0
  60. package/dist/src/extensions/wiki-link/index.js +3 -0
  61. package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
  62. package/dist/src/extensions/wiki-link/syntax.js +121 -0
  63. package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
  64. package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
  65. package/dist/src/extensions/wiki-link/types.d.ts +34 -0
  66. package/dist/src/extensions/wiki-link/types.js +1 -0
  67. package/dist/src/format.d.ts +8 -0
  68. package/dist/src/format.js +43 -0
  69. package/dist/src/graph-export.d.ts +60 -0
  70. package/dist/src/graph-export.js +309 -0
  71. package/dist/src/init-version.d.ts +10 -0
  72. package/dist/src/init-version.js +49 -0
  73. package/dist/src/knowledge/bd.d.ts +2 -0
  74. package/dist/src/knowledge/bd.js +107 -0
  75. package/dist/src/knowledge/claude-memory.d.ts +13 -0
  76. package/dist/src/knowledge/claude-memory.js +204 -0
  77. package/dist/src/knowledge/cq.d.ts +2 -0
  78. package/dist/src/knowledge/cq.js +74 -0
  79. package/dist/src/knowledge/index.d.ts +49 -0
  80. package/dist/src/knowledge/index.js +271 -0
  81. package/dist/src/knowledge/ranking.d.ts +17 -0
  82. package/dist/src/knowledge/ranking.js +62 -0
  83. package/dist/src/knowledge/session.d.ts +46 -0
  84. package/dist/src/knowledge/session.js +203 -0
  85. package/dist/src/knowledge/types.d.ts +47 -0
  86. package/dist/src/knowledge/types.js +10 -0
  87. package/dist/src/lattice.d.ts +108 -0
  88. package/dist/src/lattice.js +816 -0
  89. package/dist/src/mcp/server.d.ts +1 -0
  90. package/dist/src/mcp/server.js +58 -0
  91. package/dist/src/parser.d.ts +3 -0
  92. package/dist/src/parser.js +18 -0
  93. package/dist/src/search/db.d.ts +16 -0
  94. package/dist/src/search/db.js +54 -0
  95. package/dist/src/search/embedder.d.ts +25 -0
  96. package/dist/src/search/embedder.js +58 -0
  97. package/dist/src/search/index.d.ts +9 -0
  98. package/dist/src/search/index.js +66 -0
  99. package/dist/src/search/search.d.ts +9 -0
  100. package/dist/src/search/search.js +16 -0
  101. package/dist/src/source-parser.d.ts +27 -0
  102. package/dist/src/source-parser.js +916 -0
  103. package/dist/src/untrusted.d.ts +24 -0
  104. package/dist/src/untrusted.js +97 -0
  105. package/dist/src/version.d.ts +7 -0
  106. package/dist/src/version.js +39 -0
  107. package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
  108. package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
  109. package/dist/src/view/client/index.html +14 -0
  110. package/dist/src/view/diagnostics.d.ts +5 -0
  111. package/dist/src/view/diagnostics.js +171 -0
  112. package/dist/src/view/git-diff.d.ts +3 -0
  113. package/dist/src/view/git-diff.js +255 -0
  114. package/dist/src/view/git.d.ts +22 -0
  115. package/dist/src/view/git.js +193 -0
  116. package/dist/src/view/highlight.d.ts +2 -0
  117. package/dist/src/view/highlight.js +3767 -0
  118. package/dist/src/view/markdown.d.ts +22 -0
  119. package/dist/src/view/markdown.js +322 -0
  120. package/dist/src/view/protocol.d.ts +89 -0
  121. package/dist/src/view/protocol.js +1 -0
  122. package/dist/src/view/references.d.ts +62 -0
  123. package/dist/src/view/references.js +345 -0
  124. package/dist/src/view/repository.d.ts +12 -0
  125. package/dist/src/view/repository.js +167 -0
  126. package/dist/src/view/search.d.ts +14 -0
  127. package/dist/src/view/search.js +64 -0
  128. package/dist/src/view/server.d.ts +20 -0
  129. package/dist/src/view/server.js +233 -0
  130. package/dist/src/view/store.d.ts +57 -0
  131. package/dist/src/view/store.js +404 -0
  132. package/dist/src/walk.d.ts +18 -0
  133. package/dist/src/walk.js +27 -0
  134. package/package.json +85 -0
  135. package/templates/AGENTS.md +118 -0
  136. package/templates/README +1 -0
  137. package/templates/cursor-rules.md +77 -0
  138. package/templates/init/lat.md +1 -0
  139. package/templates/logo-dark.svg +182 -0
  140. package/templates/logo-light.svg +182 -0
  141. package/templates/logo.txt +7 -0
  142. package/templates/opencode-plugin.ts +180 -0
  143. package/templates/pi-extension.ts +364 -0
  144. package/templates/skill/SKILL.md +189 -0
@@ -0,0 +1,432 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { dirname, extname } from 'node:path';
3
+ import { findLatticeDir, loadAllSections, findSections, } from '../lattice.js';
4
+ import { plainStyler } from '../context.js';
5
+ import { expandPrompt } from './expand.js';
6
+ import { runSearch } from './search.js';
7
+ import { getSection, formatSectionOutput } from './section.js';
8
+ import { checkMd, checkCodeRefs, checkIndex, checkSections } from './check.js';
9
+ import { checkMode } from './check-mode.js';
10
+ import { SOURCE_EXTENSIONS } from '../source-parser.js';
11
+ import { federateTags, taggedDocsForFiles } from '../knowledge/index.js';
12
+ import { loadSessionMarkers, saveSessionMarkers, } from '../knowledge/session.js';
13
+ import { computeCommentReminder, } from './comment-reminder.js';
14
+ import { handlePreToolUse } from './comment-guard.js';
15
+ function outputPromptSubmit(context) {
16
+ process.stdout.write(JSON.stringify({
17
+ hookSpecificOutput: {
18
+ hookEventName: 'UserPromptSubmit',
19
+ additionalContext: context,
20
+ },
21
+ }));
22
+ }
23
+ function outputPostToolUse(eventName, context) {
24
+ process.stdout.write(JSON.stringify({
25
+ hookSpecificOutput: {
26
+ hookEventName: eventName,
27
+ additionalContext: context,
28
+ },
29
+ }));
30
+ }
31
+ /**
32
+ * Cursor reads a flat `additional_context` field, where Claude and Codex read
33
+ * a nested `hookSpecificOutput`. Same reminder, different envelope.
34
+ */
35
+ function outputCursorPostToolUse(context) {
36
+ process.stdout.write(JSON.stringify({ additional_context: context }));
37
+ }
38
+ function outputStop(reason) {
39
+ process.stdout.write(JSON.stringify({
40
+ decision: 'block',
41
+ reason,
42
+ }));
43
+ }
44
+ function outputCursorStop(reason) {
45
+ process.stdout.write(JSON.stringify({
46
+ followup_message: reason,
47
+ }));
48
+ }
49
+ async function readStdin() {
50
+ const chunks = [];
51
+ for await (const chunk of process.stdin) {
52
+ chunks.push(chunk);
53
+ }
54
+ return Buffer.concat(chunks).toString('utf-8');
55
+ }
56
+ function hasWikiLinks(text) {
57
+ return /\[\[[^\]]+\]\]/.test(text);
58
+ }
59
+ function makeHookCtx(latDir) {
60
+ return {
61
+ latDir,
62
+ projectRoot: dirname(latDir),
63
+ styler: plainStyler,
64
+ mode: 'cli',
65
+ };
66
+ }
67
+ async function searchAndExpand(ctx, userPrompt, preloadedSections) {
68
+ let result;
69
+ try {
70
+ // Read-only: search an existing index but never build/update it here. A fresh
71
+ // repo's first prompt must not trigger a full local embed pass — that's what
72
+ // `lat search` / `lat reindex` are for. Returns no matches until then.
73
+ result = await runSearch(ctx.latDir, userPrompt, 5, undefined, {
74
+ buildIndex: false,
75
+ preloadedSections,
76
+ });
77
+ }
78
+ catch {
79
+ // No usable backend (e.g. reindex required, key rejected) — skip semantic
80
+ // enrichment silently rather than blocking the user's prompt.
81
+ return { text: null, filePaths: [] };
82
+ }
83
+ const filePaths = result.matches.map((m) => m.section.filePath);
84
+ if (result.matches.length === 0)
85
+ return { text: null, filePaths };
86
+ const parts = [
87
+ `Search results for the user prompt (${result.matches.length} matches):`,
88
+ '',
89
+ ];
90
+ for (const match of result.matches) {
91
+ const sectionResult = await getSection(ctx, match.section.id);
92
+ if (sectionResult.kind === 'found') {
93
+ parts.push(formatSectionOutput(ctx, sectionResult));
94
+ parts.push('');
95
+ }
96
+ }
97
+ return { text: parts.join('\n'), filePaths };
98
+ }
99
+ async function handleUserPromptSubmit() {
100
+ let userPrompt = '';
101
+ let sessionId;
102
+ try {
103
+ const raw = await readStdin();
104
+ const input = JSON.parse(raw);
105
+ userPrompt = input.user_prompt ?? input.prompt ?? '';
106
+ sessionId = input.session_id;
107
+ }
108
+ catch {
109
+ // If we can't parse stdin, still emit the reminder
110
+ }
111
+ const parts = [];
112
+ parts.push("Before starting work, run `lat search` with one or more queries describing the user's intent.", 'ALWAYS do this, even when the task seems straightforward — search results may reveal critical design details, protocols, or constraints.', 'Use `lat section` to read the full content of relevant matches.', 'Do not read files, write code, or run commands until you have searched.', '', 'Remember: `lat.md/` must stay in sync with meaningful codebase state. If you change behavior, architecture, tests, or planned work, update the relevant current-state sections and run `lat check` before finishing. Do not use `lat.md/` as a journal/changelog or add notes for insignificant details.');
113
+ const latDir = findLatticeDir();
114
+ if (latDir && userPrompt) {
115
+ const ctx = makeHookCtx(latDir);
116
+ // Loaded once and threaded through expansion, search, and federation
117
+ // below — each of those otherwise walks and parses the whole tree itself.
118
+ const allSections = await loadAllSections(latDir);
119
+ // If the user prompt contains [[refs]], resolve them inline
120
+ if (hasWikiLinks(userPrompt)) {
121
+ try {
122
+ const expanded = await expandPrompt(ctx, userPrompt, allSections);
123
+ if (expanded) {
124
+ parts.push('', 'Expanded user prompt with resolved [[refs]]:', expanded);
125
+ }
126
+ else {
127
+ parts.push('', 'NOTE: The user prompt contains [[refs]] but they could not be resolved. Ask the user to correct them.');
128
+ }
129
+ }
130
+ catch {
131
+ parts.push('', 'NOTE: The user prompt contains [[refs]] but resolution failed. Run `lat expand` on the prompt text manually.');
132
+ }
133
+ }
134
+ // Search for relevant sections and include their full content
135
+ let searchFilePaths = [];
136
+ try {
137
+ const searchContext = await searchAndExpand(ctx, userPrompt, allSections);
138
+ searchFilePaths = searchContext.filePaths;
139
+ if (searchContext.text) {
140
+ parts.push('', searchContext.text);
141
+ }
142
+ }
143
+ catch {
144
+ // Search failed (no key, index error, etc.) — agent can search manually
145
+ }
146
+ // Federate tagged-document knowledge: documents named by [[refs]] in the
147
+ // prompt, then documents behind the semantic search matches. Wrapped in
148
+ // one try/catch, like every other enrichment above — a failing store
149
+ // lookup must never fail the user's prompt.
150
+ try {
151
+ const filePaths = [];
152
+ // [[refs]] resolution must work with no embedding index built, so it
153
+ // reuses findSections (the same resolver expandPrompt uses) rather
154
+ // than any semantic search path.
155
+ const refTargets = [...userPrompt.matchAll(/\[\[([^\]]+)\]\]/g)].map((m) => m[1]);
156
+ if (refTargets.length > 0) {
157
+ for (const target of refTargets) {
158
+ const matches = findSections(allSections, target);
159
+ if (matches.length > 0) {
160
+ filePaths.push(matches[0].section.filePath);
161
+ }
162
+ }
163
+ }
164
+ // Reuses what searchAndExpand already matched. Searching again here
165
+ // would double the cost of every prompt, and an unusable index makes
166
+ // runSearch throw — which abandoned the [[ref]] paths collected above,
167
+ // even though resolving those never touches the index at all.
168
+ filePaths.push(...searchFilePaths);
169
+ const docs = await taggedDocsForFiles(ctx.projectRoot, filePaths);
170
+ const sessionMarkers = loadSessionMarkers(sessionId);
171
+ const federated = await federateTags(docs, {
172
+ projectRoot: ctx.projectRoot,
173
+ seen: sessionMarkers.markers.seen,
174
+ attemptedEmpty: sessionMarkers.markers.attemptedEmpty,
175
+ });
176
+ saveSessionMarkers(sessionMarkers);
177
+ if (federated) {
178
+ parts.push('', federated);
179
+ }
180
+ }
181
+ catch {
182
+ // No stores available, no tagged docs, etc. — skip federation silently.
183
+ }
184
+ }
185
+ outputPromptSubmit(parts.join('\n'));
186
+ }
187
+ /**
188
+ * Reminds the agent of the `// @lat:` convention when it writes a comment
189
+ * into code via Edit/Write. See `computeCommentReminder` for the rules.
190
+ * Deliberately a reminder, not a gate: any failure here (bad stdin, fs
191
+ * errors) is swallowed and simply produces no output — it must never fail
192
+ * the edit that triggered it.
193
+ */
194
+ async function handlePostToolUse() {
195
+ try {
196
+ const raw = await readStdin();
197
+ const input = JSON.parse(raw);
198
+ const eventName = input.hook_event_name ?? 'PostToolUse';
199
+ const toolName = input.tool_name ?? '';
200
+ if (!/^(Edit|Write|MultiEdit)$/.test(toolName))
201
+ return;
202
+ const context = computeCommentReminder(input);
203
+ if (context)
204
+ outputPostToolUse(eventName, context);
205
+ }
206
+ catch {
207
+ // Never fail the tool call over a malformed payload or an fs error.
208
+ }
209
+ }
210
+ /**
211
+ * Cursor's postToolUse payload names things differently from Claude's: the
212
+ * path and content sit at the top level of `tool_input` under keys Cursor
213
+ * chooses, and the tool is `Write` rather than `Edit`/`Write`/`MultiEdit`.
214
+ * Normalize onto the shape `computeCommentReminder` already understands
215
+ * rather than teaching that function a second dialect.
216
+ */
217
+ async function handleCursorPostToolUse() {
218
+ try {
219
+ const raw = await readStdin();
220
+ const input = JSON.parse(raw);
221
+ if (!/^(Write|Edit|MultiEdit)$/i.test(input.tool_name ?? ''))
222
+ return;
223
+ const ti = input.tool_input ?? {};
224
+ const filePath = (ti['file_path'] ?? ti['path'] ?? ti['target_file']);
225
+ if (!filePath)
226
+ return;
227
+ const content = (ti['content'] ?? ti['contents'] ?? ti['new_string']);
228
+ const context = computeCommentReminder({
229
+ hook_event_name: 'postToolUse',
230
+ session_id: input.conversation_id,
231
+ cwd: input.cwd,
232
+ tool_name: 'Write',
233
+ tool_input: {
234
+ file_path: filePath,
235
+ ...(content === undefined ? {} : { content }),
236
+ ...(Array.isArray(ti['edits'])
237
+ ? { edits: ti['edits'] }
238
+ : {}),
239
+ },
240
+ });
241
+ if (context)
242
+ outputCursorPostToolUse(context);
243
+ }
244
+ catch {
245
+ // Never fail the tool call over a malformed payload or an fs error.
246
+ }
247
+ }
248
+ /** Minimum diff size (in lines) to consider "significant" code change. */
249
+ /** Minimum code change size (lines) before we consider flagging lat.md/ sync. */
250
+ const DIFF_THRESHOLD = 5;
251
+ /** lat.md/ changes below this ratio of code changes trigger a sync reminder. */
252
+ const LATMD_RATIO = 0.05;
253
+ /** If lat.md/ changes exceed this many lines, skip the ratio check entirely. */
254
+ const LATMD_UPPER_THRESHOLD = 50;
255
+ /** Run `git diff --numstat` and return { codeLines, latMdLines }. */
256
+ function analyzeDiff(projectRoot) {
257
+ let output;
258
+ try {
259
+ output = execSync('git diff HEAD --numstat', {
260
+ cwd: projectRoot,
261
+ encoding: 'utf-8',
262
+ timeout: 2000,
263
+ });
264
+ }
265
+ catch {
266
+ return { codeLines: 0, latMdLines: 0 };
267
+ }
268
+ let codeLines = 0;
269
+ let latMdLines = 0;
270
+ // Each line: "added\tremoved\tfile" (e.g. "42\t11\tsrc/cli/hook.ts")
271
+ for (const line of output.split('\n')) {
272
+ const parts = line.split('\t');
273
+ if (parts.length < 3)
274
+ continue;
275
+ const added = parseInt(parts[0], 10) || 0;
276
+ const removed = parseInt(parts[1], 10) || 0;
277
+ const file = parts[2];
278
+ const changed = added + removed;
279
+ if (file.startsWith('lat.md/')) {
280
+ latMdLines += changed;
281
+ }
282
+ else if (SOURCE_EXTENSIONS.has(extname(file))) {
283
+ codeLines += changed;
284
+ }
285
+ }
286
+ return { codeLines, latMdLines };
287
+ }
288
+ async function getStopStatus(latDir) {
289
+ const md = await checkMd(latDir);
290
+ const code = await checkCodeRefs(latDir);
291
+ const indexErrors = await checkIndex(latDir);
292
+ const sectionErrors = await checkSections(latDir);
293
+ // Must stay in step with `checkAllCommand` in check.ts. A check counted
294
+ // there but not here lets the Stop hook call a failing tree clean, which
295
+ // silently disables the very enforcement the check was added for.
296
+ const modeErrors = await checkMode(latDir);
297
+ const totalErrors = md.errors.length +
298
+ code.errors.length +
299
+ indexErrors.length +
300
+ sectionErrors.length +
301
+ modeErrors.length;
302
+ const checkFailed = totalErrors > 0;
303
+ const projectRoot = dirname(latDir);
304
+ const { codeLines, latMdLines } = analyzeDiff(projectRoot);
305
+ let needsSync = false;
306
+ if (codeLines >= DIFF_THRESHOLD && latMdLines < LATMD_UPPER_THRESHOLD) {
307
+ const effectiveLatMd = latMdLines === 0 ? 0 : Math.max(latMdLines, 1);
308
+ needsSync = effectiveLatMd < codeLines * LATMD_RATIO;
309
+ }
310
+ return {
311
+ checkFailed,
312
+ totalErrors,
313
+ needsSync,
314
+ codeLines,
315
+ latMdLines,
316
+ };
317
+ }
318
+ function formatStopReason({ checkFailed, totalErrors, needsSync, codeLines, latMdLines, }) {
319
+ if (!checkFailed && !needsSync)
320
+ return null;
321
+ const parts = [];
322
+ const syncMsg = latMdLines === 0
323
+ ? 'The codebase has changes (' +
324
+ codeLines +
325
+ ' lines) but `lat.md/` was not updated.'
326
+ : 'The codebase has changes (' +
327
+ codeLines +
328
+ ' lines) but `lat.md/` may not be fully in sync (' +
329
+ latMdLines +
330
+ ' lines changed).';
331
+ if (checkFailed && needsSync) {
332
+ parts.push('`lat check` found errors. ' + syncMsg + ' Before finishing:', '', '1. Update `lat.md/` where changes affect behavior, architecture, tests, or plans; keep it focused on current state rather than journal/changelog notes.', '2. Run `lat check` until it passes.');
333
+ }
334
+ else if (checkFailed) {
335
+ parts.push('`lat check` found ' +
336
+ totalErrors +
337
+ ' error(s). Run `lat check`, fix the errors, and repeat until it passes.');
338
+ }
339
+ else {
340
+ parts.push(syncMsg +
341
+ ' Review whether `lat.md/` needs a current-state update; do not add journal/changelog notes just to satisfy this reminder. Run `lat search` to find relevant sections and `lat check` at the end.');
342
+ }
343
+ return parts.join('\n');
344
+ }
345
+ async function handleStop() {
346
+ const latDir = findLatticeDir();
347
+ if (!latDir)
348
+ return;
349
+ // Read stdin to check if we already blocked once
350
+ let stopHookActive = false;
351
+ try {
352
+ const raw = await readStdin();
353
+ const input = JSON.parse(raw);
354
+ stopHookActive = input.stop_hook_active ?? false;
355
+ }
356
+ catch {
357
+ // If we can't parse stdin, treat as first attempt
358
+ }
359
+ const status = await getStopStatus(latDir);
360
+ // Second pass — warn the user but don't block again
361
+ if (stopHookActive) {
362
+ if (status.checkFailed) {
363
+ console.error(`lat check is still failing (${status.totalErrors} error(s)). Run \`lat check\` to see details.`);
364
+ }
365
+ return;
366
+ }
367
+ const reason = formatStopReason(status);
368
+ if (!reason)
369
+ return;
370
+ outputStop(reason);
371
+ }
372
+ async function handleCursorStop() {
373
+ const latDir = findLatticeDir();
374
+ if (!latDir)
375
+ return;
376
+ const reason = formatStopReason(await getStopStatus(latDir));
377
+ if (!reason)
378
+ return;
379
+ outputCursorStop(reason);
380
+ }
381
+ export async function hookCmd(agent, event) {
382
+ switch (agent) {
383
+ case 'claude':
384
+ switch (event) {
385
+ case 'UserPromptSubmit':
386
+ await handleUserPromptSubmit();
387
+ return;
388
+ case 'Stop':
389
+ await handleStop();
390
+ return;
391
+ case 'PreToolUse':
392
+ await handlePreToolUse(readStdin);
393
+ return;
394
+ case 'PostToolUse':
395
+ await handlePostToolUse();
396
+ return;
397
+ default:
398
+ console.error(`Unknown hook event for claude: ${event}. Supported: UserPromptSubmit, PreToolUse, Stop, PostToolUse`);
399
+ process.exit(1);
400
+ }
401
+ case 'codex':
402
+ switch (event) {
403
+ case 'UserPromptSubmit':
404
+ await handleUserPromptSubmit();
405
+ return;
406
+ case 'Stop':
407
+ await handleStop();
408
+ return;
409
+ case 'PostToolUse':
410
+ await handlePostToolUse();
411
+ return;
412
+ default:
413
+ console.error(`Unknown hook event for codex: ${event}. Supported: UserPromptSubmit, Stop, PostToolUse`);
414
+ process.exit(1);
415
+ }
416
+ case 'cursor':
417
+ switch (event) {
418
+ case 'stop':
419
+ await handleCursorStop();
420
+ return;
421
+ case 'postToolUse':
422
+ await handleCursorPostToolUse();
423
+ return;
424
+ default:
425
+ console.error(`Unknown hook event for cursor: ${event}. Supported: stop, postToolUse`);
426
+ process.exit(1);
427
+ }
428
+ default:
429
+ console.error(`Unknown agent: ${agent}. Supported: claude, codex, cursor`);
430
+ process.exit(1);
431
+ }
432
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};