@ulpi/cli 0.1.0

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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth-PN7TMQHV-2W4ICG64.js +15 -0
  4. package/dist/chunk-247GVVKK.js +2259 -0
  5. package/dist/chunk-2CLNOKPA.js +793 -0
  6. package/dist/chunk-2HEE5OKX.js +79 -0
  7. package/dist/chunk-2MZER6ND.js +415 -0
  8. package/dist/chunk-3SBPZRB5.js +772 -0
  9. package/dist/chunk-4VNS5WPM.js +42 -0
  10. package/dist/chunk-6JCMYYBT.js +1546 -0
  11. package/dist/chunk-6OCEY7JY.js +422 -0
  12. package/dist/chunk-74WVVWJ4.js +375 -0
  13. package/dist/chunk-7AL4DOEJ.js +131 -0
  14. package/dist/chunk-7LXY5UVC.js +330 -0
  15. package/dist/chunk-DBMUNBNB.js +3048 -0
  16. package/dist/chunk-JWUUVXIV.js +13694 -0
  17. package/dist/chunk-KIKPIH6N.js +4048 -0
  18. package/dist/chunk-KLEASXUR.js +70 -0
  19. package/dist/chunk-MIAQVCFW.js +39 -0
  20. package/dist/chunk-NNUWU6CV.js +1610 -0
  21. package/dist/chunk-PKD4ASEM.js +115 -0
  22. package/dist/chunk-Q4HIY43N.js +4230 -0
  23. package/dist/chunk-QJ5GSMEC.js +146 -0
  24. package/dist/chunk-SIAQVRKG.js +2163 -0
  25. package/dist/chunk-SPOI23SB.js +197 -0
  26. package/dist/chunk-YM2HV4IA.js +505 -0
  27. package/dist/codemap-RRJIDBQ5.js +636 -0
  28. package/dist/config-EGAXXCGL.js +127 -0
  29. package/dist/dist-6G7JC2RA.js +90 -0
  30. package/dist/dist-7LHZ65GC.js +418 -0
  31. package/dist/dist-LZKZFPVX.js +140 -0
  32. package/dist/dist-R5F4MX3I.js +107 -0
  33. package/dist/dist-R5ZJ4LX5.js +56 -0
  34. package/dist/dist-RJGCUS3L.js +87 -0
  35. package/dist/dist-RKOGLK7R.js +151 -0
  36. package/dist/dist-W7K4WPAF.js +597 -0
  37. package/dist/export-import-4A5MWLIA.js +53 -0
  38. package/dist/history-ATTUKOHO.js +934 -0
  39. package/dist/index.js +2120 -0
  40. package/dist/init-AY5C2ZAS.js +393 -0
  41. package/dist/launchd-LF2QMSKZ.js +148 -0
  42. package/dist/log-TVTUXAYD.js +75 -0
  43. package/dist/mcp-installer-NQCGKQ23.js +124 -0
  44. package/dist/memory-J3G24QHS.js +406 -0
  45. package/dist/ollama-3XCUZMZT-FYKHW4TZ.js +7 -0
  46. package/dist/openai-E7G2YAHU-UYY4ZWON.js +8 -0
  47. package/dist/projects-ATHDD3D6.js +271 -0
  48. package/dist/review-ADUPV3PN.js +152 -0
  49. package/dist/rules-E427DKYJ.js +134 -0
  50. package/dist/server-MOYPE4SM-N7SE2AN7.js +18 -0
  51. package/dist/server-X5P6WH2M-7K2RY34N.js +11 -0
  52. package/dist/skills/ulpi-generate-guardian/SKILL.md +511 -0
  53. package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +692 -0
  54. package/dist/skills/ulpi-generate-guardian/references/language-rules.md +596 -0
  55. package/dist/skills-CX73O3IV.js +76 -0
  56. package/dist/status-4DFHDJMN.js +66 -0
  57. package/dist/templates/biome.yml +24 -0
  58. package/dist/templates/conventional-commits.yml +18 -0
  59. package/dist/templates/django.yml +30 -0
  60. package/dist/templates/docker.yml +30 -0
  61. package/dist/templates/eslint.yml +13 -0
  62. package/dist/templates/express.yml +20 -0
  63. package/dist/templates/fastapi.yml +23 -0
  64. package/dist/templates/git-flow.yml +26 -0
  65. package/dist/templates/github-flow.yml +27 -0
  66. package/dist/templates/go.yml +33 -0
  67. package/dist/templates/jest.yml +24 -0
  68. package/dist/templates/laravel.yml +30 -0
  69. package/dist/templates/monorepo.yml +26 -0
  70. package/dist/templates/nestjs.yml +21 -0
  71. package/dist/templates/nextjs.yml +31 -0
  72. package/dist/templates/nodejs.yml +33 -0
  73. package/dist/templates/npm.yml +15 -0
  74. package/dist/templates/php.yml +25 -0
  75. package/dist/templates/pnpm.yml +15 -0
  76. package/dist/templates/prettier.yml +23 -0
  77. package/dist/templates/prisma.yml +21 -0
  78. package/dist/templates/python.yml +33 -0
  79. package/dist/templates/quality-of-life.yml +111 -0
  80. package/dist/templates/ruby.yml +25 -0
  81. package/dist/templates/rust.yml +34 -0
  82. package/dist/templates/typescript.yml +14 -0
  83. package/dist/templates/vitest.yml +24 -0
  84. package/dist/templates/yarn.yml +15 -0
  85. package/dist/templates-U7T6MARD.js +156 -0
  86. package/dist/ui-L7UAWXDY.js +167 -0
  87. package/dist/ui.html +698 -0
  88. package/dist/ulpi-RMMCUAGP-JCJ273T6.js +161 -0
  89. package/dist/uninstall-6SW35IK4.js +25 -0
  90. package/dist/update-M2B4RLGH.js +61 -0
  91. package/dist/version-checker-ANCS3IHR.js +10 -0
  92. package/package.json +92 -0
package/dist/index.js ADDED
@@ -0,0 +1,2120 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ discoverUlpiServer,
4
+ registerWithServer,
5
+ waitForServerDecision
6
+ } from "./chunk-2MZER6ND.js";
7
+ import {
8
+ captureCommitDiff,
9
+ extractSections,
10
+ extractTitle,
11
+ getSectionFullText,
12
+ isGitCommitCommand,
13
+ parseMarkdownToBlocks
14
+ } from "./chunk-3SBPZRB5.js";
15
+ import {
16
+ detectStack
17
+ } from "./chunk-2CLNOKPA.js";
18
+ import {
19
+ getDefaultProject,
20
+ getProject,
21
+ registerProject
22
+ } from "./chunk-SPOI23SB.js";
23
+ import {
24
+ injectSkill,
25
+ loadSkillSync
26
+ } from "./chunk-6OCEY7JY.js";
27
+ import {
28
+ evaluateRules,
29
+ loadRulesSync,
30
+ matchesFilePattern
31
+ } from "./chunk-SIAQVRKG.js";
32
+ import {
33
+ buildPrePromptSnapshot,
34
+ buildSessionSummary,
35
+ entryExists,
36
+ findReviewPlansForCommit,
37
+ getCommitDiffStats,
38
+ getCommitMetadata,
39
+ getCommitRawDiff,
40
+ getCurrentHead,
41
+ historyBranchExists,
42
+ listCommitsBetween,
43
+ loadActiveGuards,
44
+ readBranchMeta,
45
+ readEntryTranscript,
46
+ readTranscript,
47
+ updateEntryTranscript,
48
+ writeHistoryEntry
49
+ } from "./chunk-NNUWU6CV.js";
50
+ import {
51
+ JsonSessionStore,
52
+ appendEvent,
53
+ createInitialState,
54
+ projectDirToSlug,
55
+ readEvents,
56
+ updateStateFromInput
57
+ } from "./chunk-YM2HV4IA.js";
58
+ import "./chunk-KIKPIH6N.js";
59
+ import {
60
+ NOTIFICATIONS_LOG_FILE,
61
+ REVIEW_FLAGS_DIR,
62
+ SESSIONS_DIR,
63
+ ULPI_GLOBAL_DIR,
64
+ getApiHost,
65
+ getApiPort,
66
+ globalGuardsFile,
67
+ loadUlpiSettings,
68
+ projectGuardsFile,
69
+ projectGuardsFileAlt,
70
+ projectNoAutoGenFile
71
+ } from "./chunk-7LXY5UVC.js";
72
+ import "./chunk-4VNS5WPM.js";
73
+
74
+ // src/index.ts
75
+ import * as fs4 from "fs";
76
+ import * as path4 from "path";
77
+
78
+ // src/hooks/handler.ts
79
+ import * as fs3 from "fs";
80
+ import * as path3 from "path";
81
+
82
+ // src/hooks/session-start.ts
83
+ import { existsSync } from "fs";
84
+ import { join } from "path";
85
+ async function handleSessionStart(ctx) {
86
+ const { input, state, store, projectDir: projectDir2 } = ctx;
87
+ try {
88
+ state.stack = detectStack(projectDir2);
89
+ } catch {
90
+ }
91
+ try {
92
+ const { execFileSync: execFileSync2 } = await import("child_process");
93
+ const branch = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
94
+ cwd: projectDir2,
95
+ encoding: "utf-8",
96
+ timeout: 3e3
97
+ }).trim();
98
+ if (branch && branch !== "HEAD") {
99
+ state.branch = branch;
100
+ }
101
+ } catch {
102
+ }
103
+ try {
104
+ const head = getCurrentHead(projectDir2);
105
+ if (head) state.headAtStart = head;
106
+ } catch {
107
+ }
108
+ state.phase = "active";
109
+ state.transcriptPath = input.transcript_path;
110
+ try {
111
+ const { execFileSync: execFileSync2 } = await import("child_process");
112
+ const untracked = execFileSync2("git", ["ls-files", "--others", "--exclude-standard"], {
113
+ cwd: projectDir2,
114
+ encoding: "utf-8",
115
+ timeout: 3e3
116
+ }).toString().trim();
117
+ state.untrackedFilesAtStart = untracked ? untracked.split("\n") : [];
118
+ try {
119
+ execFileSync2("git", ["diff", "--quiet"], { cwd: projectDir2, timeout: 3e3 });
120
+ state.workingTreeDirtyAtStart = false;
121
+ } catch {
122
+ state.workingTreeDirtyAtStart = true;
123
+ }
124
+ } catch {
125
+ }
126
+ if (!state.sessionName) {
127
+ const now = /* @__PURE__ */ new Date();
128
+ const dateStr = now.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
129
+ state.sessionName = state.branch ? `${state.branch} - ${dateStr}` : dateStr;
130
+ }
131
+ store.save(state);
132
+ appendEvent(input.session_id, {
133
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
134
+ event: "session_start",
135
+ hookEvent: input.hook_event_name,
136
+ message: `Session started in ${projectDir2}`
137
+ }, projectDir2);
138
+ if (shouldPromptForGeneration(projectDir2)) {
139
+ outputGenerationPrompt();
140
+ }
141
+ import("./version-checker-ANCS3IHR.js").then((m) => m.checkForUpdates()).catch(() => {
142
+ });
143
+ try {
144
+ const { isMemoryEnabled, loadMemoryConfig, getTopMemories, formatMemoriesForAgent } = await import("./dist-R5F4MX3I.js");
145
+ if (isMemoryEnabled(projectDir2)) {
146
+ const config = loadMemoryConfig(projectDir2);
147
+ if (config.surfaceOnStart) {
148
+ const memories = getTopMemories(projectDir2, config.surfaceLimit);
149
+ if (memories.length > 0) {
150
+ const formatted = formatMemoriesForAgent(memories);
151
+ process.stderr.write(formatted + "\n");
152
+ }
153
+ }
154
+ }
155
+ } catch {
156
+ }
157
+ try {
158
+ const { loadCodemapConfig } = await import("./dist-LZKZFPVX.js");
159
+ const { getCodemapBranch, getCurrentBranch } = await import("./dist-RKOGLK7R.js");
160
+ const { historyBranchExists: historyBranchExists2 } = await import("./dist-RJGCUS3L.js");
161
+ const codemapConfig = loadCodemapConfig(projectDir2);
162
+ if (codemapConfig.autoImport) {
163
+ const branch = state.branch ?? getCurrentBranch(projectDir2);
164
+ const shadowBranch = getCodemapBranch(branch);
165
+ if (historyBranchExists2(projectDir2, shadowBranch)) {
166
+ const { importIndex } = await import("./dist-LZKZFPVX.js");
167
+ await importIndex(projectDir2, branch);
168
+ }
169
+ }
170
+ } catch {
171
+ }
172
+ }
173
+ function shouldPromptForGeneration(projectDir2) {
174
+ const rulesYml = projectGuardsFile(projectDir2);
175
+ const rulesYaml = projectGuardsFileAlt(projectDir2);
176
+ if (existsSync(rulesYml) || existsSync(rulesYaml)) {
177
+ return false;
178
+ }
179
+ const hasPackageJson = existsSync(join(projectDir2, "package.json"));
180
+ const hasPyProject = existsSync(join(projectDir2, "pyproject.toml"));
181
+ const hasCargoToml = existsSync(join(projectDir2, "Cargo.toml"));
182
+ const hasComposerJson = existsSync(join(projectDir2, "composer.json"));
183
+ const hasGemfile = existsSync(join(projectDir2, "Gemfile"));
184
+ const hasGoMod = existsSync(join(projectDir2, "go.mod"));
185
+ if (!hasPackageJson && !hasPyProject && !hasCargoToml && !hasComposerJson && !hasGemfile && !hasGoMod) {
186
+ return false;
187
+ }
188
+ if (hasBeenDismissed(projectDir2)) {
189
+ return false;
190
+ }
191
+ return true;
192
+ }
193
+ function hasBeenDismissed(projectDir2) {
194
+ const dismissFile = projectNoAutoGenFile(projectDir2);
195
+ return existsSync(dismissFile);
196
+ }
197
+ function outputGenerationPrompt() {
198
+ const message = `
199
+ \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
200
+ \u2502 ULPI Not Configured \u2502
201
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
202
+ \u2502 \u2502
203
+ \u2502 Would you like to auto-generate hooks for this \u2502
204
+ \u2502 project? This will: \u2502
205
+ \u2502 \u2502
206
+ \u2502 \u2022 Detect your stack (language, framework, tools) \u2502
207
+ \u2502 \u2022 Generate optimized guards.yml \u2502
208
+ \u2502 \u2022 Auto-approve safe operations \u2502
209
+ \u2502 \u2022 Block dangerous commands \u2502
210
+ \u2502 \u2502
211
+ \u2502 To generate: Type "/ulpi-generate-guardian" or say \u2502
212
+ \u2502 "yes, generate guardian" \u2502
213
+ \u2502 \u2502
214
+ \u2502 After generating, manage hooks via web UI: \u2502
215
+ \u2502 ulpi ui \u2502
216
+ \u2502 \u2192 http://localhost:${getApiPort()} \u2502
217
+ \u2502 \u2502
218
+ \u2502 To dismiss: Create empty .ulpi/.no-auto-gen \u2502
219
+ \u2502 file \u2502
220
+ \u2502 \u2502
221
+ \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F
222
+ `;
223
+ process.stderr.write(message);
224
+ }
225
+
226
+ // ../../packages/notifications-engine/dist/index.js
227
+ import { execFileSync } from "child_process";
228
+ import { writeFileSync, unlinkSync } from "fs";
229
+ import { tmpdir } from "os";
230
+ import { join as join2 } from "path";
231
+ import * as os from "os";
232
+ import * as fs from "fs";
233
+ import * as path from "path";
234
+ import * as fs2 from "fs";
235
+ import * as path2 from "path";
236
+ function classifyNotification(input) {
237
+ if (input.notification_type) {
238
+ return input.notification_type;
239
+ }
240
+ const msg = (input.message ?? "").toLowerCase();
241
+ if (msg.includes("permission")) return "permission_prompt";
242
+ if (msg.includes("idle") || msg.includes("waiting")) return "idle_prompt";
243
+ if (msg.includes("auth")) return "auth_success";
244
+ if (msg.includes("elicitation") || msg.includes("input")) return "elicitation_dialog";
245
+ return "unknown";
246
+ }
247
+ var NotificationDeduplicator = class _NotificationDeduplicator {
248
+ seen = /* @__PURE__ */ new Map();
249
+ windowMs;
250
+ maxEntries;
251
+ checkCount = 0;
252
+ static CLEANUP_INTERVAL = 100;
253
+ constructor(windowMs = 5e3, maxEntries = 1e3) {
254
+ this.windowMs = windowMs;
255
+ this.maxEntries = maxEntries;
256
+ }
257
+ /**
258
+ * Check if this notification should be suppressed.
259
+ * Returns true if the notification is a duplicate (should be suppressed).
260
+ */
261
+ isDuplicate(key) {
262
+ const now = Date.now();
263
+ this.checkCount++;
264
+ if (this.checkCount >= _NotificationDeduplicator.CLEANUP_INTERVAL || this.seen.size > this.maxEntries) {
265
+ this.cleanup(now);
266
+ this.checkCount = 0;
267
+ }
268
+ const lastSeen = this.seen.get(key);
269
+ if (lastSeen !== void 0 && now - lastSeen < this.windowMs) {
270
+ return true;
271
+ }
272
+ this.seen.set(key, now);
273
+ if (this.seen.size > this.maxEntries) {
274
+ this.evictOldest();
275
+ }
276
+ return false;
277
+ }
278
+ /**
279
+ * Remove expired entries.
280
+ */
281
+ cleanup(now) {
282
+ for (const [key, ts] of this.seen) {
283
+ if (now - ts >= this.windowMs) {
284
+ this.seen.delete(key);
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Evict the oldest half of entries when hard cap is exceeded.
290
+ */
291
+ evictOldest() {
292
+ const entries = Array.from(this.seen.entries()).sort((a, b) => a[1] - b[1]);
293
+ const toRemove = Math.floor(entries.length / 2);
294
+ for (let i = 0; i < toRemove; i++) {
295
+ this.seen.delete(entries[i][0]);
296
+ }
297
+ }
298
+ /** Update the deduplication time window. */
299
+ setWindowMs(ms) {
300
+ this.windowMs = ms;
301
+ }
302
+ /** Reset all state. */
303
+ clear() {
304
+ this.seen.clear();
305
+ this.checkCount = 0;
306
+ }
307
+ };
308
+ function sendDesktopNotification(title, message) {
309
+ const platform2 = os.platform();
310
+ try {
311
+ if (platform2 === "darwin") {
312
+ const escapedBody = message.replace(/["\\]/g, "\\$&");
313
+ const escapedTitle = title.replace(/["\\]/g, "\\$&");
314
+ const script = `display notification "${escapedBody}" with title "${escapedTitle}"`;
315
+ execFileSync("osascript", ["-e", script], {
316
+ timeout: 5e3,
317
+ stdio: "ignore"
318
+ });
319
+ return true;
320
+ }
321
+ if (platform2 === "linux") {
322
+ execFileSync("notify-send", [title, message], {
323
+ timeout: 5e3,
324
+ stdio: "ignore"
325
+ });
326
+ return true;
327
+ }
328
+ if (platform2 === "win32") {
329
+ const escapedTitle = title.replace(/'/g, "''");
330
+ const escapedBody = message.replace(/'/g, "''");
331
+ const ps = [
332
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
333
+ "$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
334
+ "$textNodes = $template.GetElementsByTagName('text')",
335
+ `$textNodes.Item(0).AppendChild($template.CreateTextNode('${escapedTitle}')) | Out-Null`,
336
+ `$textNodes.Item(1).AppendChild($template.CreateTextNode('${escapedBody}')) | Out-Null`,
337
+ "$toast = [Windows.UI.Notifications.ToastNotification]::new($template)",
338
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ULPI').Show($toast)"
339
+ ].join("\n");
340
+ const tmpFile = join2(tmpdir(), `ulpi-notify-${Date.now()}.ps1`);
341
+ try {
342
+ writeFileSync(tmpFile, ps);
343
+ execFileSync(
344
+ "powershell",
345
+ ["-ExecutionPolicy", "Bypass", "-File", tmpFile],
346
+ { timeout: 5e3, stdio: "ignore" }
347
+ );
348
+ } finally {
349
+ try {
350
+ unlinkSync(tmpFile);
351
+ } catch {
352
+ }
353
+ }
354
+ return true;
355
+ }
356
+ return false;
357
+ } catch {
358
+ return false;
359
+ }
360
+ }
361
+ function validateWebhookUrl(url) {
362
+ const parsed = new URL(url);
363
+ const hostname = parsed.hostname.toLowerCase();
364
+ const blocked = [
365
+ "localhost",
366
+ "127.0.0.1",
367
+ "0.0.0.0",
368
+ "::1",
369
+ "[::1]",
370
+ "169.254.169.254"
371
+ // AWS/GCP metadata
372
+ ];
373
+ if (blocked.includes(hostname)) {
374
+ throw new Error(`Webhook URL targets blocked address: ${hostname}`);
375
+ }
376
+ if (hostname.startsWith("10.") || hostname.startsWith("192.168.") || hostname.startsWith("172.16.") || hostname.startsWith("172.17.") || hostname.startsWith("172.18.") || hostname.startsWith("172.19.") || hostname.startsWith("172.2") || hostname.startsWith("172.30.") || hostname.startsWith("172.31.")) {
377
+ throw new Error(`Webhook URL targets blocked address: ${hostname}`);
378
+ }
379
+ if (hostname.endsWith(".local") || hostname.endsWith(".internal")) {
380
+ throw new Error(`Webhook URL targets blocked address: ${hostname}`);
381
+ }
382
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
383
+ throw new Error("Webhook URL must use http or https protocol");
384
+ }
385
+ }
386
+ async function sendWebhookNotification(url, payload, headers) {
387
+ try {
388
+ validateWebhookUrl(url);
389
+ const response = await fetch(url, {
390
+ method: "POST",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ "User-Agent": "ULPI/1.0",
394
+ ...headers
395
+ },
396
+ body: JSON.stringify(payload),
397
+ signal: AbortSignal.timeout(5e3)
398
+ });
399
+ return response.ok;
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+ function validateLogPath(logPath) {
405
+ const resolvedPath = path.resolve(logPath);
406
+ const resolvedBase = path.resolve(ULPI_GLOBAL_DIR);
407
+ if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
408
+ throw new Error("Log path must be within ULPI config directory");
409
+ }
410
+ }
411
+ function logNotification(entry, logPath) {
412
+ const target = logPath ?? NOTIFICATIONS_LOG_FILE;
413
+ try {
414
+ if (logPath) {
415
+ validateLogPath(logPath);
416
+ }
417
+ const dir = path.dirname(target);
418
+ fs.mkdirSync(dir, { recursive: true });
419
+ const line = JSON.stringify(entry) + "\n";
420
+ fs.appendFileSync(target, line, "utf-8");
421
+ return true;
422
+ } catch {
423
+ return false;
424
+ }
425
+ }
426
+ function sendTerminalBell() {
427
+ process.stderr.write("\x07");
428
+ }
429
+ function writeTerminalNotification(title, message) {
430
+ process.stderr.write(`
431
+ [ulpi] ${title}: ${message}
432
+ `);
433
+ }
434
+ var deduplicator = new NotificationDeduplicator();
435
+ function getEventConfig(classified, config) {
436
+ const key = classified;
437
+ const value = config[key];
438
+ if (value && typeof value === "object" && "notify" in value) {
439
+ return value;
440
+ }
441
+ return void 0;
442
+ }
443
+ function collectActions(eventConfig) {
444
+ const actions = [];
445
+ const act = eventConfig.act;
446
+ if (!act) return actions;
447
+ if (act.inject_skill) actions.push(`inject_skill:${act.inject_skill}`);
448
+ if (act.inject_skill_on_retry) actions.push("inject_skill_on_retry");
449
+ if (act.inject_skill_after_n_blocks != null) actions.push("inject_skill_after_n_blocks");
450
+ if (act.alert_after_n_blocks != null) actions.push("alert_after_n_blocks");
451
+ if (act.rate_limit) actions.push("rate_limit");
452
+ if (act.log_failure_details) actions.push("log_failure_details");
453
+ return actions;
454
+ }
455
+ async function dispatchNotification(eventType, title, message, config) {
456
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
457
+ const windowMs = config?.dedup_window_seconds ? config.dedup_window_seconds * 1e3 : 5e3;
458
+ deduplicator.setWindowMs(windowMs);
459
+ const dedupeKey = `${eventType}:${message}`;
460
+ if (deduplicator.isDuplicate(dedupeKey)) {
461
+ return { classified: eventType, channels: [], actions: [], suppressed: true };
462
+ }
463
+ const channels = [];
464
+ const actions = [];
465
+ const eventConfig = config ? getEventConfig(eventType, config) : void 0;
466
+ if (eventConfig) {
467
+ for (const channel of eventConfig.notify) {
468
+ const sent = await routeToChannel(channel, title, message, timestamp, eventType);
469
+ if (sent) channels.push(channel.type);
470
+ }
471
+ actions.push(...collectActions(eventConfig));
472
+ } else {
473
+ logNotification({ event: eventType, title, message, timestamp });
474
+ channels.push("log");
475
+ }
476
+ return {
477
+ classified: eventType,
478
+ channels,
479
+ actions,
480
+ suppressed: false,
481
+ actConfig: eventConfig?.act
482
+ };
483
+ }
484
+ async function routeNotification(input, config) {
485
+ const classified = classifyNotification(input);
486
+ const title = input.title ?? "ULPI";
487
+ const message = input.message ?? classified;
488
+ return dispatchNotification(classified, title, message, config);
489
+ }
490
+ async function fireNotification(eventType, title, message, config) {
491
+ return dispatchNotification(eventType, title, message, config);
492
+ }
493
+ async function routeToChannel(channel, title, message, timestamp, event) {
494
+ const displayMessage = channel.message ?? message;
495
+ switch (channel.type) {
496
+ case "desktop":
497
+ return sendDesktopNotification(title, displayMessage);
498
+ case "terminal":
499
+ if (channel.sound !== false) {
500
+ sendTerminalBell();
501
+ }
502
+ writeTerminalNotification(title, displayMessage);
503
+ return true;
504
+ case "log": {
505
+ const logPath = channel.path ?? void 0;
506
+ return logNotification(
507
+ { event, title, message: displayMessage, timestamp },
508
+ logPath
509
+ );
510
+ }
511
+ case "webhook": {
512
+ if (!channel.url) return false;
513
+ return sendWebhookNotification(channel.url, {
514
+ event,
515
+ title,
516
+ message: displayMessage,
517
+ timestamp
518
+ });
519
+ }
520
+ default:
521
+ return false;
522
+ }
523
+ }
524
+ function evaluateNotificationTriggers(state, latestEvent, config, recentEvents) {
525
+ if (!config) return [];
526
+ const triggers = [];
527
+ const ruleBlockedTrigger = checkRuleBlocked(latestEvent, config);
528
+ if (ruleBlockedTrigger) triggers.push(ruleBlockedTrigger);
529
+ const repeatedTrigger = checkRepeatedFailures(state, config);
530
+ if (repeatedTrigger) triggers.push(repeatedTrigger);
531
+ const sensitiveTrigger = checkSensitivePathAccess(
532
+ latestEvent,
533
+ state,
534
+ config
535
+ );
536
+ if (sensitiveTrigger) triggers.push(sensitiveTrigger);
537
+ const rapidTrigger = checkRapidPermissionRequests(recentEvents, config);
538
+ if (rapidTrigger) triggers.push(rapidTrigger);
539
+ const taskFailedTrigger = checkTaskFailed(latestEvent, config);
540
+ if (taskFailedTrigger) triggers.push(taskFailedTrigger);
541
+ const taskCompleteTrigger = checkTaskComplete(latestEvent, state, config);
542
+ if (taskCompleteTrigger) triggers.push(taskCompleteTrigger);
543
+ return triggers;
544
+ }
545
+ function checkRuleBlocked(latestEvent, config) {
546
+ if (!config.rule_blocked) return null;
547
+ if (latestEvent.type !== "tool_blocked") return null;
548
+ const tool = latestEvent.toolName ?? "unknown tool";
549
+ const rule = latestEvent.ruleName ?? "unknown rule";
550
+ const detail = latestEvent.message ? `: ${latestEvent.message}` : "";
551
+ return {
552
+ eventType: "rule_blocked",
553
+ title: "ULPI",
554
+ message: `Blocked ${tool} by rule "${rule}"${detail}`,
555
+ urgency: config.rule_blocked.urgency
556
+ };
557
+ }
558
+ function checkRepeatedFailures(state, config) {
559
+ if (!config.repeated_failures) return null;
560
+ const threshold = config.repeated_failures.threshold ?? 3;
561
+ const blocks = state.consecutiveBlocks;
562
+ if (blocks < threshold) return null;
563
+ if (blocks % threshold !== 0) return null;
564
+ return {
565
+ eventType: "repeated_failures",
566
+ title: "ULPI",
567
+ message: `${blocks} consecutive blocks detected (threshold: ${threshold}). The agent may be stuck in a retry loop.`,
568
+ urgency: config.repeated_failures.urgency
569
+ };
570
+ }
571
+ function checkSensitivePathAccess(latestEvent, state, config) {
572
+ if (!config.sensitive_path_access) return null;
573
+ const patterns = config.sensitive_path_access.file_patterns;
574
+ if (!patterns || patterns.length === 0) return null;
575
+ const filePath = latestEvent.filePath;
576
+ if (!filePath) return null;
577
+ const projectDir2 = state.projectDir || ".";
578
+ const matched = patterns.some(
579
+ (pattern) => matchesFilePattern(pattern, filePath, projectDir2)
580
+ );
581
+ if (!matched) return null;
582
+ return {
583
+ eventType: "sensitive_path_access",
584
+ title: "ULPI",
585
+ message: `Sensitive file accessed: ${filePath}`,
586
+ urgency: config.sensitive_path_access.urgency
587
+ };
588
+ }
589
+ function checkRapidPermissionRequests(recentEvents, config) {
590
+ if (!config.rapid_permission_requests) return null;
591
+ if (!recentEvents || recentEvents.length === 0) return null;
592
+ const windowSeconds = config.rapid_permission_requests.window_seconds ?? 60;
593
+ const threshold = config.rapid_permission_requests.threshold ?? 5;
594
+ const now = Date.now();
595
+ const windowMs = windowSeconds * 1e3;
596
+ const permissionEvents = recentEvents.filter((evt) => {
597
+ if (evt.event !== "permission_allow" && evt.event !== "permission_deny") {
598
+ return false;
599
+ }
600
+ const evtTime = new Date(evt.ts).getTime();
601
+ return now - evtTime <= windowMs;
602
+ });
603
+ if (permissionEvents.length < threshold) return null;
604
+ return {
605
+ eventType: "rapid_permission_requests",
606
+ title: "ULPI",
607
+ message: `${permissionEvents.length} permission requests in the last ${windowSeconds}s (threshold: ${threshold}). The agent may be requesting excessive permissions.`,
608
+ urgency: config.rapid_permission_requests.urgency
609
+ };
610
+ }
611
+ function checkTaskFailed(latestEvent, config) {
612
+ if (!config.task_failed) return null;
613
+ if (latestEvent.type !== "postcondition_failed") return null;
614
+ const command2 = latestEvent.command ?? "unknown command";
615
+ const detail = latestEvent.message ? `: ${latestEvent.message}` : "";
616
+ return {
617
+ eventType: "task_failed",
618
+ title: "ULPI",
619
+ message: `Postcondition failed for "${command2}"${detail}`,
620
+ urgency: config.task_failed.urgency
621
+ };
622
+ }
623
+ function checkTaskComplete(latestEvent, state, config) {
624
+ if (!config.task_complete) return null;
625
+ if (latestEvent.type !== "session_end") return null;
626
+ if (state.filesWritten.length === 0) return null;
627
+ const filesWritten = state.filesWritten.length;
628
+ const blocked = state.actionsBlocked;
629
+ const blockNote = blocked > 0 ? ` (${blocked} action${blocked === 1 ? "" : "s"} blocked)` : "";
630
+ return {
631
+ eventType: "task_complete",
632
+ title: "ULPI",
633
+ message: `Session complete: ${filesWritten} file${filesWritten === 1 ? "" : "s"} written${blockNote}.`,
634
+ urgency: config.task_complete.urgency
635
+ };
636
+ }
637
+ var rateLimitCounters = /* @__PURE__ */ new Map();
638
+ function isRateLimited(eventType, threshold, windowSeconds) {
639
+ const now = Date.now();
640
+ const windowMs = windowSeconds * 1e3;
641
+ const entry = rateLimitCounters.get(eventType);
642
+ if (!entry || now - entry.windowStart >= windowMs) {
643
+ rateLimitCounters.set(eventType, { count: 1, windowStart: now });
644
+ return false;
645
+ }
646
+ entry.count++;
647
+ if (entry.count > threshold) {
648
+ return true;
649
+ }
650
+ return false;
651
+ }
652
+ function executeActions(act, ctx) {
653
+ const result = {
654
+ stderrMessage: ctx.stderrMessage,
655
+ extraChannels: [],
656
+ rateLimitSuppressed: false,
657
+ failureDetailsLogged: false
658
+ };
659
+ if (!act) return result;
660
+ const blocks = ctx.state.consecutiveBlocks;
661
+ if (act.rate_limit) {
662
+ if (isRateLimited(ctx.eventType, act.rate_limit.threshold, act.rate_limit.window_seconds)) {
663
+ result.rateLimitSuppressed = true;
664
+ return result;
665
+ }
666
+ }
667
+ if (act.inject_skill) {
668
+ result.stderrMessage = appendSkill(result.stderrMessage, act.inject_skill, ctx.projectDir);
669
+ }
670
+ if (act.inject_skill_on_retry && blocks > 0) {
671
+ result.stderrMessage = appendSkill(result.stderrMessage, act.inject_skill_on_retry, ctx.projectDir);
672
+ }
673
+ if (act.inject_skill_after_n_blocks != null && blocks >= act.inject_skill_after_n_blocks && act.inject_skill) {
674
+ result.stderrMessage = appendSkill(result.stderrMessage, act.inject_skill, ctx.projectDir);
675
+ }
676
+ if (act.alert_after_n_blocks != null && blocks >= act.alert_after_n_blocks) {
677
+ if (act.alert_channels && act.alert_channels.length > 0) {
678
+ result.extraChannels = act.alert_channels;
679
+ }
680
+ }
681
+ if (act.log_failure_details && ctx.latestEvent) {
682
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
683
+ logNotification({
684
+ event: `${ctx.eventType}:failure_details`,
685
+ title: "Failure Details",
686
+ message: JSON.stringify({
687
+ toolName: ctx.latestEvent.toolName,
688
+ toolInput: ctx.latestEvent.toolInput,
689
+ ruleName: ctx.latestEvent.ruleName,
690
+ ruleMessage: ctx.latestEvent.message,
691
+ consecutiveBlocks: blocks,
692
+ filesWritten: ctx.state.filesWritten.length,
693
+ commandsRun: ctx.state.commandsRun.length
694
+ }),
695
+ timestamp
696
+ });
697
+ result.failureDetailsLogged = true;
698
+ }
699
+ return result;
700
+ }
701
+ function appendSkill(message, skillPath, projectDir2) {
702
+ const content = loadSkillSync(skillPath, projectDir2);
703
+ if (!content) return message;
704
+ if (!message) return content;
705
+ return `${message}
706
+
707
+ ---
708
+
709
+ ${content}`;
710
+ }
711
+ function auditNotification(entry, projectDir2, baseDir) {
712
+ try {
713
+ const slug = projectDirToSlug(projectDir2);
714
+ const dir = baseDir ? path2.join(baseDir, "sessions", slug) : path2.join(SESSIONS_DIR, slug);
715
+ fs2.mkdirSync(dir, { recursive: true });
716
+ const filePath = path2.join(dir, "notifications.jsonl");
717
+ const line = JSON.stringify(entry) + "\n";
718
+ fs2.appendFileSync(filePath, line, "utf-8");
719
+ } catch {
720
+ }
721
+ }
722
+
723
+ // src/hooks/notify-with-actions.ts
724
+ async function fireWithActions(triggers, responses, ctx) {
725
+ let stderrMessage = ctx.stderrMessage;
726
+ for (const trigger of triggers) {
727
+ try {
728
+ const routeResult = await fireNotification(
729
+ trigger.eventType,
730
+ trigger.title,
731
+ trigger.message,
732
+ responses
733
+ );
734
+ const actionCtx = {
735
+ eventType: trigger.eventType,
736
+ state: ctx.state,
737
+ projectDir: ctx.projectDir,
738
+ stderrMessage,
739
+ latestEvent: ctx.latestEvent ? {
740
+ toolName: ctx.latestEvent.toolName,
741
+ toolInput: ctx.latestEvent.toolInput,
742
+ ruleName: ctx.latestEvent.ruleName,
743
+ message: ctx.latestEvent.message
744
+ } : void 0
745
+ };
746
+ const actionResult = executeActions(routeResult.actConfig, actionCtx);
747
+ if (actionResult.stderrMessage !== stderrMessage) {
748
+ stderrMessage = actionResult.stderrMessage;
749
+ }
750
+ if (actionResult.extraChannels.length > 0) {
751
+ for (const channel of actionResult.extraChannels) {
752
+ await dispatchToChannel(channel, trigger.title, trigger.message);
753
+ }
754
+ }
755
+ const auditEntry = {
756
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
757
+ sessionId: ctx.sessionId,
758
+ eventType: trigger.eventType,
759
+ urgency: trigger.urgency,
760
+ title: trigger.title,
761
+ message: trigger.message,
762
+ channels: routeResult.channels,
763
+ actions: routeResult.actions,
764
+ suppressed: routeResult.suppressed || actionResult.rateLimitSuppressed,
765
+ suppressReason: routeResult.suppressed ? "dedup" : actionResult.rateLimitSuppressed ? "rate_limit" : void 0,
766
+ triggerContext: ctx.latestEvent ? {
767
+ toolName: ctx.latestEvent.toolName,
768
+ filePath: ctx.latestEvent.filePath,
769
+ command: ctx.latestEvent.command,
770
+ ruleName: ctx.latestEvent.ruleName,
771
+ consecutiveBlocks: ctx.state.consecutiveBlocks
772
+ } : void 0
773
+ };
774
+ auditNotification(auditEntry, ctx.projectDir);
775
+ } catch {
776
+ }
777
+ }
778
+ return { stderrMessage };
779
+ }
780
+ async function dispatchToChannel(channel, title, message) {
781
+ const displayMessage = channel.message ?? message;
782
+ switch (channel.type) {
783
+ case "desktop":
784
+ await sendDesktopNotification(title, displayMessage);
785
+ break;
786
+ case "terminal":
787
+ if (channel.sound !== false) sendTerminalBell();
788
+ writeTerminalNotification(title, displayMessage);
789
+ break;
790
+ case "log":
791
+ logNotification({
792
+ event: "alert_escalation",
793
+ title,
794
+ message: displayMessage,
795
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
796
+ }, channel.path ?? void 0);
797
+ break;
798
+ case "webhook":
799
+ if (channel.url) {
800
+ await sendWebhookNotification(channel.url, {
801
+ event: "alert_escalation",
802
+ title,
803
+ message: displayMessage,
804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
805
+ });
806
+ }
807
+ break;
808
+ }
809
+ }
810
+
811
+ // src/hooks/review-integration.ts
812
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, readdirSync, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
813
+ import { homedir } from "os";
814
+ import { join as join4 } from "path";
815
+ var FLAGS_BASE = REVIEW_FLAGS_DIR;
816
+ function flagsDir(sessionId) {
817
+ return sessionId ? join4(FLAGS_BASE, sessionId) : FLAGS_BASE;
818
+ }
819
+ function ensureFlagsDir(sessionId) {
820
+ const dir = flagsDir(sessionId);
821
+ if (!existsSync2(dir)) mkdirSync3(dir, { recursive: true });
822
+ }
823
+ function getReviewSettings() {
824
+ return loadUlpiSettings().review;
825
+ }
826
+ function isReviewEnabled(type) {
827
+ const review = getReviewSettings();
828
+ if (!review.enabled) return false;
829
+ if (type === "plan") return review.plan_review;
830
+ if (type === "code") return review.code_review;
831
+ return false;
832
+ }
833
+ async function extractPlanForReview(input) {
834
+ if (input.tool_input?.plan && typeof input.tool_input.plan === "string") {
835
+ return input.tool_input.plan;
836
+ }
837
+ if (input.tool_input?.planFilePath && typeof input.tool_input.planFilePath === "string") {
838
+ try {
839
+ return readFileSync(input.tool_input.planFilePath, "utf-8");
840
+ } catch {
841
+ }
842
+ }
843
+ if (input.transcript_path) {
844
+ const planFromTranscript = extractPlanFromTranscript(input.transcript_path);
845
+ if (planFromTranscript) return planFromTranscript;
846
+ }
847
+ const planDirs = [
848
+ input.cwd ? join4(input.cwd, ".claude", "plans") : null,
849
+ join4(homedir(), ".claude", "plans")
850
+ ].filter(Boolean);
851
+ for (const dir of planDirs) {
852
+ const planFromDir = extractPlanFromDirectory(dir);
853
+ if (planFromDir) return planFromDir;
854
+ }
855
+ return null;
856
+ }
857
+ function extractPlanFromDirectory(plansDir) {
858
+ try {
859
+ if (!existsSync2(plansDir)) return null;
860
+ const files = readdirSync(plansDir);
861
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
862
+ if (mdFiles.length === 0) return null;
863
+ let latest = { file: mdFiles[0], mtime: 0 };
864
+ for (const f of mdFiles) {
865
+ const stat = statSync(join4(plansDir, f));
866
+ if (stat.mtimeMs > latest.mtime) {
867
+ latest = { file: f, mtime: stat.mtimeMs };
868
+ }
869
+ }
870
+ const content = readFileSync(join4(plansDir, latest.file), "utf-8");
871
+ if (content.trim()) {
872
+ console.error(`[ulpi] Reading plan from ${plansDir}/${latest.file}`);
873
+ return content;
874
+ }
875
+ } catch {
876
+ }
877
+ return null;
878
+ }
879
+ function extractPlanFromTranscript(transcriptPath) {
880
+ try {
881
+ const transcriptText = readFileSync(transcriptPath, "utf-8");
882
+ const lines = transcriptText.trim().split("\n").reverse();
883
+ for (const line of lines) {
884
+ try {
885
+ const entry = JSON.parse(line);
886
+ if (entry.role === "assistant" && typeof entry.content === "string") {
887
+ if (looksLikePlan(entry.content)) return entry.content;
888
+ }
889
+ if (Array.isArray(entry.content)) {
890
+ for (const block of entry.content) {
891
+ if (block.type === "text" && typeof block.text === "string") {
892
+ if (looksLikePlan(block.text)) return block.text;
893
+ }
894
+ }
895
+ }
896
+ } catch {
897
+ }
898
+ }
899
+ } catch {
900
+ }
901
+ return null;
902
+ }
903
+ function looksLikePlan(content) {
904
+ return content.includes("#") && content.length > 100;
905
+ }
906
+ function buildRichFeedback(decision, sections) {
907
+ const sectionTitleMap = /* @__PURE__ */ new Map();
908
+ for (const s of sections) {
909
+ sectionTitleMap.set(s.id, s.title);
910
+ }
911
+ const resolveSectionTitle = (sectionId) => {
912
+ if (!sectionId || sectionId === "global" || sectionId === "__global__") return "Global";
913
+ const byId = sectionTitleMap.get(sectionId);
914
+ if (byId) return byId;
915
+ const slugMatch = sections.find(
916
+ (s) => s.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") === sectionId
917
+ );
918
+ if (slugMatch) return slugMatch.title;
919
+ return sectionId;
920
+ };
921
+ const header = decision.behavior === "deny" ? "PLAN REVIEW FEEDBACK \u2014 Changes Requested" : "PLAN REVIEW FEEDBACK \u2014 Approved with Notes";
922
+ const parts = [header, ""];
923
+ const annotations = decision.annotations || [];
924
+ if (annotations.length > 0) {
925
+ parts.push("## Annotations");
926
+ const grouped = /* @__PURE__ */ new Map();
927
+ for (const ann of annotations) {
928
+ const key = ann.sectionId || "__global__";
929
+ const group = grouped.get(key);
930
+ if (group) {
931
+ group.push(ann);
932
+ } else {
933
+ grouped.set(key, [ann]);
934
+ }
935
+ }
936
+ for (const [key, anns] of grouped) {
937
+ for (const ann of anns) {
938
+ const sectionLabel = key === "__global__" ? "Global" : `Section: "${resolveSectionTitle(ann.sectionId)}"`;
939
+ const typeLabel = ann.type.toUpperCase().replace("_", " ");
940
+ parts.push(`### ${sectionLabel} (${typeLabel})`);
941
+ parts.push(ann.text);
942
+ if (ann.imagePaths && ann.imagePaths.length > 0) {
943
+ for (const imgPath of ann.imagePaths) {
944
+ parts.push(` - Attached image: ${imgPath}`);
945
+ }
946
+ }
947
+ parts.push("");
948
+ }
949
+ }
950
+ }
951
+ const priorities = decision.priorities || [];
952
+ if (priorities.length > 0) {
953
+ parts.push("## Priorities");
954
+ for (const p of priorities) {
955
+ const title = resolveSectionTitle(p.sectionId);
956
+ const note = p.note ? ` (${p.note})` : "";
957
+ parts.push(`- "${title}" \u2192 ${p.priority.toUpperCase()}${note}`);
958
+ }
959
+ parts.push("");
960
+ }
961
+ const risks = decision.risks || [];
962
+ if (risks.length > 0) {
963
+ parts.push("## Risks");
964
+ for (const r of risks) {
965
+ const title = resolveSectionTitle(r.sectionId);
966
+ parts.push(`- "${title}" \u2192 ${r.level.toUpperCase()}: ${r.description}`);
967
+ }
968
+ parts.push("");
969
+ }
970
+ const instructions = decision.instructions || [];
971
+ if (instructions.length > 0) {
972
+ parts.push("## Instructions");
973
+ for (const inst of instructions) {
974
+ const title = resolveSectionTitle(inst.sectionId);
975
+ const priorityTag = inst.priority ? ` [${inst.priority.toUpperCase()}]` : "";
976
+ parts.push(`- "${title}": ${inst.instruction}${priorityTag}`);
977
+ }
978
+ parts.push("");
979
+ }
980
+ const inlineEdits = decision.inlineEdits || [];
981
+ if (inlineEdits.length > 0) {
982
+ parts.push("## Inline Edits");
983
+ for (const edit of inlineEdits) {
984
+ const title = resolveSectionTitle(edit.sectionId);
985
+ parts.push(`- Section "${title}": Reviewer edited the implementation approach`);
986
+ parts.push(` Original: "${truncate(edit.originalContent, 120)}"`);
987
+ parts.push(` Changed to: "${truncate(edit.editedContent, 120)}"`);
988
+ }
989
+ parts.push("");
990
+ }
991
+ const message = decision.message || decision.feedback;
992
+ if (message) {
993
+ parts.push("## Reviewer Message");
994
+ parts.push(message);
995
+ parts.push("");
996
+ }
997
+ if (decision.behavior === "deny") {
998
+ parts.push("ADDRESS ALL FEEDBACK BEFORE RE-SUBMITTING THE PLAN.");
999
+ }
1000
+ return parts.join("\n");
1001
+ }
1002
+ function truncate(text, maxLen) {
1003
+ const singleLine = text.replace(/\n/g, " ").trim();
1004
+ if (singleLine.length <= maxLen) return singleLine;
1005
+ return singleLine.slice(0, maxLen - 3) + "...";
1006
+ }
1007
+ async function runPlanReviewSession(plan, projectDir2, sessionId) {
1008
+ try {
1009
+ const discovered = await discoverUlpiServer();
1010
+ if (!discovered) {
1011
+ const settings2 = getReviewSettings();
1012
+ if (settings2.require_server) {
1013
+ console.error("[ulpi] No ULPI server running \u2014 blocking plan exit (require_server=true). Start the server with: ulpi ui");
1014
+ return { behavior: "deny", message: "Review server is required but not running. Start it with: ulpi ui" };
1015
+ }
1016
+ console.error("[ulpi] No ULPI server running \u2014 skipping plan review");
1017
+ return { behavior: "allow" };
1018
+ }
1019
+ const { port, secret: apiSecret } = discovered;
1020
+ appendEvent(sessionId, {
1021
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1022
+ event: "review_plan_started",
1023
+ hookEvent: "PermissionRequest",
1024
+ toolName: "ExitPlanMode",
1025
+ message: "Plan review session started"
1026
+ }, projectDir2);
1027
+ const registration = await registerWithServer(port, {
1028
+ type: "plan",
1029
+ plan,
1030
+ projectPath: projectDir2
1031
+ }, apiSecret);
1032
+ if (!registration) {
1033
+ console.error("[ulpi] Failed to register plan review session");
1034
+ return { behavior: "allow" };
1035
+ }
1036
+ console.error(`[ulpi] Plan review session: ${registration.sessionId}`);
1037
+ const apiHost = getApiHost();
1038
+ const planTokenParam = registration.token ? `&token=${encodeURIComponent(registration.token)}` : "";
1039
+ console.error(`[ulpi] Waiting for review decision at http://${apiHost}:${port}/review/plan?session=${registration.sessionId}${planTokenParam}`);
1040
+ const settings = getReviewSettings();
1041
+ const maxWaitMs = settings.review_timeout_seconds > 0 ? settings.review_timeout_seconds * 1e3 : void 0;
1042
+ const decision = await waitForServerDecision(port, registration.sessionId, 3e4, maxWaitMs, registration.token, apiSecret);
1043
+ if (!decision) {
1044
+ const behavior = settings.timeout_behavior ?? "allow";
1045
+ console.error(`[ulpi] Plan review timed out \u2014 ${behavior}`);
1046
+ return { behavior };
1047
+ }
1048
+ const planDecision = decision;
1049
+ appendEvent(sessionId, {
1050
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1051
+ event: "review_plan_decided",
1052
+ hookEvent: "PermissionRequest",
1053
+ toolName: "ExitPlanMode",
1054
+ message: `Plan review: ${planDecision.behavior}`
1055
+ }, projectDir2);
1056
+ const hasReviewData = (planDecision.annotations?.length ?? 0) > 0 || (planDecision.priorities?.length ?? 0) > 0 || (planDecision.risks?.length ?? 0) > 0 || (planDecision.instructions?.length ?? 0) > 0 || (planDecision.inlineEdits?.length ?? 0) > 0;
1057
+ let feedback;
1058
+ if (hasReviewData) {
1059
+ const blocks = parseMarkdownToBlocks(plan);
1060
+ const planSections = extractSections(blocks);
1061
+ feedback = buildRichFeedback(planDecision, planSections);
1062
+ } else {
1063
+ feedback = planDecision.feedback || planDecision.message || "";
1064
+ }
1065
+ return {
1066
+ behavior: planDecision.behavior,
1067
+ message: planDecision.message,
1068
+ feedback,
1069
+ clearContext: planDecision.clearContext
1070
+ };
1071
+ } catch (err) {
1072
+ console.error(`[ulpi] Plan review error: ${err instanceof Error ? err.message : err}`);
1073
+ return { behavior: "allow" };
1074
+ }
1075
+ }
1076
+ async function runCodeReviewSession(diff, commitMessage, projectDir2, sessionId) {
1077
+ try {
1078
+ const discovered = await discoverUlpiServer();
1079
+ if (!discovered) {
1080
+ const settings2 = getReviewSettings();
1081
+ if (settings2.require_server) {
1082
+ console.error("[ulpi] No ULPI server running \u2014 blocking commit (require_server=true). Start the server with: ulpi ui");
1083
+ return { approved: false, message: "Review server is required but not running. Start it with: ulpi ui" };
1084
+ }
1085
+ console.error("[ulpi] No ULPI server running \u2014 skipping code review");
1086
+ return { approved: true };
1087
+ }
1088
+ const { port, secret: apiSecret } = discovered;
1089
+ appendEvent(sessionId, {
1090
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1091
+ event: "review_code_started",
1092
+ hookEvent: "PreToolUse",
1093
+ toolName: "Bash",
1094
+ command: "git commit",
1095
+ message: "Code review session started"
1096
+ }, projectDir2);
1097
+ const registration = await registerWithServer(port, {
1098
+ type: "code",
1099
+ diff,
1100
+ commitMessage,
1101
+ projectPath: projectDir2
1102
+ }, apiSecret);
1103
+ if (!registration) {
1104
+ console.error("[ulpi] Failed to register code review session");
1105
+ return { approved: true };
1106
+ }
1107
+ console.error(`[ulpi] Code review session: ${registration.sessionId}`);
1108
+ const codeApiHost = getApiHost();
1109
+ const codeTokenParam = registration.token ? `&token=${encodeURIComponent(registration.token)}` : "";
1110
+ console.error(`[ulpi] Waiting for review decision at http://${codeApiHost}:${port}/review/code?session=${registration.sessionId}${codeTokenParam}`);
1111
+ const settings = getReviewSettings();
1112
+ const maxWaitMs = settings.review_timeout_seconds > 0 ? settings.review_timeout_seconds * 1e3 : void 0;
1113
+ const decision = await waitForServerDecision(port, registration.sessionId, 3e4, maxWaitMs, registration.token, apiSecret);
1114
+ if (!decision) {
1115
+ const behavior = settings.timeout_behavior ?? "allow";
1116
+ console.error(`[ulpi] Code review timed out \u2014 ${behavior}`);
1117
+ return { approved: behavior === "allow" };
1118
+ }
1119
+ const codeDecision = decision;
1120
+ appendEvent(sessionId, {
1121
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1122
+ event: "review_code_decided",
1123
+ hookEvent: "PreToolUse",
1124
+ toolName: "Bash",
1125
+ command: "git commit",
1126
+ message: `Code review: ${codeDecision.approved ? "approved" : "changes requested"}`
1127
+ }, projectDir2);
1128
+ return {
1129
+ approved: codeDecision.approved,
1130
+ feedback: codeDecision.feedback,
1131
+ message: codeDecision.message
1132
+ };
1133
+ } catch (err) {
1134
+ console.error(`[ulpi] Code review error: ${err instanceof Error ? err.message : err}`);
1135
+ return { approved: true };
1136
+ }
1137
+ }
1138
+ function writeClearContextFlag(sessionId) {
1139
+ ensureFlagsDir(sessionId);
1140
+ writeFileSync2(join4(flagsDir(sessionId), "clear-context.flag"), JSON.stringify({ timestamp: Date.now() }));
1141
+ }
1142
+ function readClearContextFlag(sessionId) {
1143
+ try {
1144
+ const path5 = join4(flagsDir(sessionId), "clear-context.flag");
1145
+ if (!existsSync2(path5)) return false;
1146
+ const data = JSON.parse(readFileSync(path5, "utf8"));
1147
+ unlinkSync2(path5);
1148
+ const age = Date.now() - (data.timestamp || 0);
1149
+ return age < 5 * 60 * 1e3;
1150
+ } catch {
1151
+ return false;
1152
+ }
1153
+ }
1154
+ function writeReviewFeedbackFlag(feedback, sessionId) {
1155
+ ensureFlagsDir(sessionId);
1156
+ writeFileSync2(join4(flagsDir(sessionId), "review-feedback.flag"), JSON.stringify({ timestamp: Date.now(), feedback }));
1157
+ }
1158
+ function readReviewFeedbackFlag(sessionId) {
1159
+ try {
1160
+ const path5 = join4(flagsDir(sessionId), "review-feedback.flag");
1161
+ if (!existsSync2(path5)) return null;
1162
+ const data = JSON.parse(readFileSync(path5, "utf8"));
1163
+ unlinkSync2(path5);
1164
+ const age = Date.now() - (data.timestamp || 0);
1165
+ if (age >= 5 * 60 * 1e3) return null;
1166
+ return data.feedback || null;
1167
+ } catch {
1168
+ return null;
1169
+ }
1170
+ }
1171
+ function writeTeamDelegationFlag(planTitle, sections, sessionId) {
1172
+ ensureFlagsDir(sessionId);
1173
+ const flag = { timestamp: Date.now(), planTitle, sections };
1174
+ writeFileSync2(join4(flagsDir(sessionId), "team-delegation.flag"), JSON.stringify(flag));
1175
+ }
1176
+ function readTeamDelegationFlag(sessionId) {
1177
+ try {
1178
+ const path5 = join4(flagsDir(sessionId), "team-delegation.flag");
1179
+ if (!existsSync2(path5)) return null;
1180
+ const data = JSON.parse(readFileSync(path5, "utf8"));
1181
+ unlinkSync2(path5);
1182
+ const age = Date.now() - (data.timestamp || 0);
1183
+ if (age >= 5 * 60 * 1e3) return null;
1184
+ return data;
1185
+ } catch {
1186
+ return null;
1187
+ }
1188
+ }
1189
+ var AGENT_TYPE_KEYWORDS = [
1190
+ { keywords: ["express", "node", "api", "endpoint", "middleware", "route", "server", "rest", "graphql"], agentType: "express-senior-engineer" },
1191
+ { keywords: ["react", "component", "tailwind", "css", "ui", "frontend", "vite", "styled", "jsx", "tsx"], agentType: "react-vite-tailwind-engineer" },
1192
+ { keywords: ["next.js", "nextjs", "app router", "rsc", "server component", "next"], agentType: "nextjs-senior-engineer" },
1193
+ { keywords: ["laravel", "php", "eloquent", "artisan", "blade", "migration"], agentType: "laravel-senior-engineer" },
1194
+ { keywords: ["docker", "container", "compose", "dockerfile", "kubernetes", "k8s"], agentType: "devops-docker-senior-engineer" },
1195
+ { keywords: ["aws", "cdk", "cloudformation", "terraform", "lambda", "s3", "ec2", "iam"], agentType: "devops-aws-senior-engineer" },
1196
+ { keywords: ["cli", "command", "terminal", "script", "commander", "inquirer", "chalk"], agentType: "nodejs-cli-senior-engineer" }
1197
+ ];
1198
+ function inferAgentType(title, content) {
1199
+ const text = `${title} ${content}`.toLowerCase();
1200
+ let bestMatch = "general-purpose";
1201
+ let bestScore = 0;
1202
+ for (const entry of AGENT_TYPE_KEYWORDS) {
1203
+ let score = 0;
1204
+ for (const keyword of entry.keywords) {
1205
+ if (text.includes(keyword)) score++;
1206
+ }
1207
+ if (score > bestScore) {
1208
+ bestScore = score;
1209
+ bestMatch = entry.agentType;
1210
+ }
1211
+ }
1212
+ return bestMatch;
1213
+ }
1214
+ function buildDelegationSections(sections, blocks) {
1215
+ return sections.filter((s) => s.level >= 2 && s.title !== "Overview").map((section) => {
1216
+ const content = getSectionFullText(section, blocks);
1217
+ return {
1218
+ title: section.title,
1219
+ agentType: inferAgentType(section.title, content)
1220
+ };
1221
+ });
1222
+ }
1223
+ function buildDelegationContext(delegation) {
1224
+ const sectionLines = delegation.sections.map((s) => `- "${s.title}" \u2192 ${s.agentType}`).join("\n");
1225
+ return `TEAM DELEGATION INSTRUCTIONS:
1226
+
1227
+ The plan "${delegation.planTitle}" has been approved. You MUST now create a team of specialized agents to execute this plan in parallel using the Task tool.
1228
+
1229
+ As the team lead, you should:
1230
+ 1. Analyze each plan section and confirm the recommended agent type (or adjust based on your understanding)
1231
+ 2. Launch parallel Task agents for independent sections in a SINGLE message with multiple Task tool calls
1232
+ 3. Use sequential execution only where sections have explicit dependencies
1233
+ 4. Coordinate results after agents complete and verify integration
1234
+
1235
+ Recommended agent allocation:
1236
+ ${sectionLines}
1237
+
1238
+ IMPORTANT: Launch independent agents in PARALLEL (single message, multiple Task tool calls) for maximum efficiency.`;
1239
+ }
1240
+ function collectPostReviewContexts(sessionId) {
1241
+ const contexts = [];
1242
+ if (readClearContextFlag(sessionId)) {
1243
+ contexts.push(
1244
+ "IMPORTANT: The plan reviewer approved this plan with a request to clear context before implementation. You MUST immediately run /compact before implementing. Do not ask the user \u2014 just do it now. After compaction completes, proceed with the approved plan."
1245
+ );
1246
+ }
1247
+ const reviewFeedback = readReviewFeedbackFlag(sessionId);
1248
+ if (reviewFeedback) {
1249
+ contexts.push(reviewFeedback);
1250
+ }
1251
+ const delegation = readTeamDelegationFlag(sessionId);
1252
+ if (delegation) {
1253
+ contexts.push(buildDelegationContext(delegation));
1254
+ }
1255
+ return contexts;
1256
+ }
1257
+
1258
+ // src/hooks/pre-tool.ts
1259
+ async function handlePreTool(ctx) {
1260
+ const { input, state, rules, projectDir: projectDir2, store } = ctx;
1261
+ const result = evaluateRules(input, rules, state, projectDir2);
1262
+ for (const { rule, outcome } of result.matchedRules) {
1263
+ appendEvent(input.session_id, {
1264
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1265
+ event: outcome === "fail" ? "tool_blocked" : "tool_allowed",
1266
+ hookEvent: input.hook_event_name,
1267
+ toolName: input.tool_name,
1268
+ filePath: input.tool_input?.file_path,
1269
+ command: input.tool_input?.command,
1270
+ ruleName: rule.id,
1271
+ message: outcome === "fail" ? result.stderrMessage : void 0
1272
+ }, projectDir2);
1273
+ }
1274
+ if (result.matchedRules.length > 0) {
1275
+ state.rulesEnforced += result.matchedRules.length;
1276
+ }
1277
+ const isBlock = result.action === "block" || result.action === "feedback";
1278
+ const failedRule = result.matchedRules.find((m) => m.outcome === "fail");
1279
+ try {
1280
+ const triggers = evaluateNotificationTriggers(state, {
1281
+ type: isBlock ? "tool_blocked" : "tool_allowed",
1282
+ toolName: input.tool_name,
1283
+ filePath: input.tool_input?.file_path,
1284
+ ruleName: failedRule?.rule?.id,
1285
+ message: result.stderrMessage
1286
+ }, rules.responses);
1287
+ const notifyResult = await fireWithActions(triggers, rules.responses, {
1288
+ sessionId: input.session_id,
1289
+ state,
1290
+ projectDir: projectDir2,
1291
+ stderrMessage: result.stderrMessage,
1292
+ latestEvent: {
1293
+ toolName: input.tool_name,
1294
+ toolInput: input.tool_input,
1295
+ filePath: input.tool_input?.file_path,
1296
+ ruleName: failedRule?.rule?.id,
1297
+ message: result.stderrMessage
1298
+ }
1299
+ });
1300
+ if (notifyResult.stderrMessage && notifyResult.stderrMessage !== result.stderrMessage) {
1301
+ result.stderrMessage = notifyResult.stderrMessage;
1302
+ }
1303
+ for (const trigger of triggers) {
1304
+ appendEvent(input.session_id, {
1305
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1306
+ event: "notification_routed",
1307
+ hookEvent: input.hook_event_name,
1308
+ message: `Notification: ${trigger.eventType} -- ${trigger.message}`
1309
+ }, projectDir2);
1310
+ }
1311
+ } catch {
1312
+ }
1313
+ if (isBlock) {
1314
+ state.actionsBlocked++;
1315
+ state.consecutiveBlocks++;
1316
+ let message = result.stderrMessage ?? "Action blocked by rule";
1317
+ if (failedRule?.rule.skill) {
1318
+ message = injectSkill(message, failedRule.rule.skill, projectDir2);
1319
+ }
1320
+ store.save(state);
1321
+ if (result.stdoutJson) {
1322
+ process.stdout.write(JSON.stringify(result.stdoutJson));
1323
+ }
1324
+ process.stderr.write(message);
1325
+ return 2;
1326
+ }
1327
+ if (input.tool_name === "Bash" && isGitCommitCommand(input.tool_input?.command) && isReviewEnabled("code")) {
1328
+ try {
1329
+ const command2 = input.tool_input?.command;
1330
+ const { diff, commitMessage, allowEmpty } = captureCommitDiff({ command: command2, cwd: projectDir2 });
1331
+ if (allowEmpty && !diff.trim()) {
1332
+ console.error("[ulpi] Skipping code review: --allow-empty commit with no changes.");
1333
+ } else if (diff.trim()) {
1334
+ const reviewResult = await runCodeReviewSession(
1335
+ diff,
1336
+ commitMessage,
1337
+ projectDir2,
1338
+ input.session_id
1339
+ );
1340
+ if (!reviewResult.approved) {
1341
+ state.actionsBlocked++;
1342
+ state.consecutiveBlocks++;
1343
+ const message = reviewResult.feedback || reviewResult.message || "Code review: changes requested.";
1344
+ store.save(state);
1345
+ process.stderr.write(message);
1346
+ return 2;
1347
+ }
1348
+ }
1349
+ } catch (err) {
1350
+ console.error(`[ulpi] Code review error: ${err instanceof Error ? err.message : err}`);
1351
+ }
1352
+ }
1353
+ state.consecutiveBlocks = 0;
1354
+ if (result.stdoutJson) {
1355
+ process.stdout.write(JSON.stringify(result.stdoutJson));
1356
+ }
1357
+ store.save(state);
1358
+ return 0;
1359
+ }
1360
+
1361
+ // src/hooks/post-tool.ts
1362
+ import * as child_process from "child_process";
1363
+ async function handlePostTool(ctx) {
1364
+ const { input, state, rules, projectDir: projectDir2, store } = ctx;
1365
+ const nextState = updateStateFromInput(state, input);
1366
+ Object.assign(state, nextState);
1367
+ if ((state.phase === "active" || state.phase === "active_committed") && state.headAtStart && historyBranchExists(projectDir2)) {
1368
+ const meta = readBranchMeta(projectDir2);
1369
+ if (meta?.config?.captureStrategy === "on-commit") {
1370
+ const currentHead = getCurrentHead(projectDir2);
1371
+ if (currentHead && currentHead !== state.headAtStart) {
1372
+ state.phase = "active_committed";
1373
+ captureOnCommit(projectDir2, state, input, currentHead).catch(() => {
1374
+ });
1375
+ state.headAtStart = currentHead;
1376
+ }
1377
+ }
1378
+ }
1379
+ try {
1380
+ const contexts = collectPostReviewContexts(input.session_id);
1381
+ if (contexts.length > 0) {
1382
+ const contextJson = {
1383
+ hookSpecificOutput: {
1384
+ hookEventName: "PostToolUse",
1385
+ additionalContext: contexts.join("\n\n---\n\n")
1386
+ }
1387
+ };
1388
+ process.stdout.write(JSON.stringify(contextJson));
1389
+ }
1390
+ } catch {
1391
+ }
1392
+ const result = evaluateRules(input, rules, state, projectDir2);
1393
+ const postconditions = result.postconditionResults ?? [];
1394
+ let hadFailure = false;
1395
+ let blockingFailure = false;
1396
+ let failedCommand = "";
1397
+ let failedMessage = "";
1398
+ if (postconditions.length > 0) {
1399
+ const failureFeedback = [];
1400
+ for (const { rule, command: cmd } of postconditions) {
1401
+ try {
1402
+ child_process.execSync(cmd, {
1403
+ cwd: projectDir2,
1404
+ encoding: "utf-8",
1405
+ timeout: rule.timeout ?? 3e4,
1406
+ stdio: ["pipe", "pipe", "pipe"]
1407
+ });
1408
+ state.autoActionsRun++;
1409
+ appendEvent(input.session_id, {
1410
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1411
+ event: "postcondition_run",
1412
+ hookEvent: input.hook_event_name,
1413
+ toolName: input.tool_name,
1414
+ command: cmd,
1415
+ message: "Postcondition succeeded"
1416
+ }, projectDir2);
1417
+ } catch (err) {
1418
+ const error = err;
1419
+ const errMsg = error.stderr || error.message || "Unknown error";
1420
+ hadFailure = true;
1421
+ failedCommand = cmd;
1422
+ failedMessage = errMsg;
1423
+ appendEvent(input.session_id, {
1424
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1425
+ event: "postcondition_failed",
1426
+ hookEvent: input.hook_event_name,
1427
+ toolName: input.tool_name,
1428
+ command: cmd,
1429
+ message: errMsg
1430
+ }, projectDir2);
1431
+ failureFeedback.push(`\u2717 ${cmd}
1432
+ ${errMsg}`);
1433
+ if (rule.block_on_failure) {
1434
+ blockingFailure = true;
1435
+ break;
1436
+ }
1437
+ }
1438
+ }
1439
+ try {
1440
+ const triggers = evaluateNotificationTriggers(state, {
1441
+ type: hadFailure ? "postcondition_failed" : "postcondition_run",
1442
+ toolName: input.tool_name,
1443
+ command: failedCommand || void 0,
1444
+ message: failedMessage || void 0
1445
+ }, rules.responses);
1446
+ await fireWithActions(triggers, rules.responses, {
1447
+ sessionId: input.session_id,
1448
+ state,
1449
+ projectDir: projectDir2,
1450
+ latestEvent: {
1451
+ toolName: input.tool_name,
1452
+ command: failedCommand || void 0,
1453
+ message: failedMessage || void 0
1454
+ }
1455
+ });
1456
+ for (const trigger of triggers) {
1457
+ appendEvent(input.session_id, {
1458
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1459
+ event: "notification_routed",
1460
+ hookEvent: input.hook_event_name,
1461
+ message: `Notification: ${trigger.eventType} -- ${trigger.message}`
1462
+ }, projectDir2);
1463
+ }
1464
+ } catch {
1465
+ }
1466
+ if (blockingFailure && failureFeedback.length > 0) {
1467
+ store.save(state);
1468
+ process.stderr.write(
1469
+ `[ulpi] Postconditions:
1470
+ ${failureFeedback.join("\n\n")}`
1471
+ );
1472
+ return 2;
1473
+ }
1474
+ }
1475
+ try {
1476
+ const { isMemoryEnabled, loadMemoryConfig, appendMemoryEvent, toClassificationEvent } = await import("./dist-R5F4MX3I.js");
1477
+ if (isMemoryEnabled(projectDir2)) {
1478
+ const memConfig = loadMemoryConfig(projectDir2);
1479
+ if (memConfig.captureMode === "continuous") {
1480
+ const classEvent = toClassificationEvent({
1481
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1482
+ event: hadFailure ? "postcondition_failed" : "tool_used",
1483
+ hookEvent: input.hook_event_name,
1484
+ toolName: input.tool_name,
1485
+ filePath: input.tool_input?.file_path,
1486
+ command: input.tool_input?.command,
1487
+ message: void 0
1488
+ });
1489
+ appendMemoryEvent(input.session_id, classEvent, projectDir2);
1490
+ }
1491
+ }
1492
+ } catch {
1493
+ }
1494
+ if (input.tool_name === "Bash" && /\bgit\s+push\b/.test(String(input.tool_input?.command ?? ""))) {
1495
+ try {
1496
+ const { loadCodemapConfig, getCodemapStatus, exportIndex } = await import("./dist-LZKZFPVX.js");
1497
+ const { getCurrentBranch } = await import("./dist-RKOGLK7R.js");
1498
+ const codemapConfig = loadCodemapConfig(projectDir2);
1499
+ if (codemapConfig.autoExport) {
1500
+ const branch = state.branch ?? getCurrentBranch(projectDir2);
1501
+ const codemapStatus = getCodemapStatus(projectDir2, branch);
1502
+ if (codemapStatus.initialized) {
1503
+ exportIndex(projectDir2, branch).catch(() => {
1504
+ });
1505
+ }
1506
+ }
1507
+ } catch {
1508
+ }
1509
+ }
1510
+ store.save(state);
1511
+ return 0;
1512
+ }
1513
+ async function captureOnCommit(projectDir2, state, input, sha) {
1514
+ if (entryExists(projectDir2, sha)) return;
1515
+ const commit = getCommitMetadata(projectDir2, sha);
1516
+ const diff = getCommitDiffStats(projectDir2, sha);
1517
+ const { diff: rawDiff, truncated } = getCommitRawDiff(projectDir2, sha);
1518
+ const events = readEvents(input.session_id, projectDir2);
1519
+ const sessionSummary = buildSessionSummary(state, events);
1520
+ const guardsYaml = loadActiveGuards(projectDir2);
1521
+ const entry = {
1522
+ version: 1,
1523
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1524
+ commit,
1525
+ diff,
1526
+ rawDiff: rawDiff || void 0,
1527
+ diffTruncated: truncated || void 0,
1528
+ session: sessionSummary,
1529
+ enrichment: null,
1530
+ reviewPlans: null,
1531
+ prePromptSnapshot: buildPrePromptSnapshot(state)
1532
+ };
1533
+ await writeHistoryEntry(projectDir2, entry, {
1534
+ state,
1535
+ events,
1536
+ guardsYaml
1537
+ });
1538
+ }
1539
+
1540
+ // src/hooks/permission.ts
1541
+ async function handlePermission(ctx) {
1542
+ const { input, state, rules, projectDir: projectDir2, store } = ctx;
1543
+ const result = evaluateRules(input, rules, state, projectDir2);
1544
+ const hookDecision = result.stdoutJson?.hookSpecificOutput?.decision?.behavior ?? result.stdoutJson?.hookSpecificOutput?.permissionDecision;
1545
+ if (hookDecision) {
1546
+ const decision = hookDecision;
1547
+ const reason = result.stdoutJson?.hookSpecificOutput?.decision?.message ?? result.stdoutJson?.hookSpecificOutput?.permissionDecisionReason;
1548
+ appendEvent(input.session_id, {
1549
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1550
+ event: decision === "allow" ? "permission_allow" : "permission_deny",
1551
+ hookEvent: input.hook_event_name,
1552
+ toolName: input.tool_name,
1553
+ filePath: input.tool_input?.file_path,
1554
+ command: input.tool_input?.command,
1555
+ message: reason
1556
+ }, projectDir2);
1557
+ if (decision === "deny") {
1558
+ state.actionsBlocked++;
1559
+ }
1560
+ try {
1561
+ const recentEvents = readEvents(input.session_id, projectDir2);
1562
+ const triggers = evaluateNotificationTriggers(state, {
1563
+ type: decision === "allow" ? "permission_allow" : "permission_deny",
1564
+ toolName: input.tool_name
1565
+ }, rules.responses, recentEvents);
1566
+ const notifyResult = await fireWithActions(triggers, rules.responses, {
1567
+ sessionId: input.session_id,
1568
+ state,
1569
+ projectDir: projectDir2,
1570
+ stderrMessage: reason,
1571
+ latestEvent: {
1572
+ toolName: input.tool_name,
1573
+ toolInput: input.tool_input,
1574
+ filePath: input.tool_input?.file_path,
1575
+ command: input.tool_input?.command
1576
+ }
1577
+ });
1578
+ if (decision === "deny" && notifyResult.stderrMessage && notifyResult.stderrMessage !== reason) {
1579
+ if (result.stderrMessage) {
1580
+ result.stderrMessage = notifyResult.stderrMessage;
1581
+ }
1582
+ }
1583
+ for (const trigger of triggers) {
1584
+ appendEvent(input.session_id, {
1585
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1586
+ event: "notification_routed",
1587
+ hookEvent: input.hook_event_name,
1588
+ message: `Notification: ${trigger.eventType} -- ${trigger.message}`
1589
+ }, projectDir2);
1590
+ }
1591
+ } catch {
1592
+ }
1593
+ store.save(state);
1594
+ process.stdout.write(JSON.stringify(result.stdoutJson));
1595
+ if (result.stderrMessage) {
1596
+ process.stderr.write(result.stderrMessage);
1597
+ }
1598
+ return 2;
1599
+ }
1600
+ if (input.tool_name === "ExitPlanMode" && isReviewEnabled("plan")) {
1601
+ console.error(`[ulpi] ExitPlanMode intercepted \u2014 extracting plan (cwd: ${input.cwd})`);
1602
+ try {
1603
+ const plan = await extractPlanForReview(input);
1604
+ console.error(`[ulpi] Plan extraction result: ${plan ? `${plan.length} chars` : "null"}`);
1605
+ if (plan) {
1606
+ const result2 = await runPlanReviewSession(plan, projectDir2, input.session_id);
1607
+ if (result2.behavior === "allow") {
1608
+ state.planApproved = true;
1609
+ if (result2.clearContext) {
1610
+ writeClearContextFlag(input.session_id);
1611
+ }
1612
+ if (result2.feedback) {
1613
+ writeReviewFeedbackFlag(result2.feedback, input.session_id);
1614
+ }
1615
+ try {
1616
+ const blocks = parseMarkdownToBlocks(plan);
1617
+ const sections = extractSections(blocks);
1618
+ const title = extractTitle(blocks);
1619
+ const delegationSections = buildDelegationSections(sections, blocks);
1620
+ if (delegationSections.length > 0) {
1621
+ writeTeamDelegationFlag(title, delegationSections, input.session_id);
1622
+ }
1623
+ } catch {
1624
+ }
1625
+ const allowJson = {
1626
+ hookSpecificOutput: {
1627
+ hookEventName: "PermissionRequest",
1628
+ decision: { behavior: "allow" }
1629
+ }
1630
+ };
1631
+ store.save(state);
1632
+ process.stdout.write(JSON.stringify(allowJson));
1633
+ return 2;
1634
+ } else {
1635
+ const feedbackText = result2.feedback || result2.message || "Plan review: changes requested";
1636
+ writeReviewFeedbackFlag(feedbackText, input.session_id);
1637
+ const denyJson = {
1638
+ hookSpecificOutput: {
1639
+ hookEventName: "PermissionRequest",
1640
+ decision: { behavior: "deny", message: feedbackText }
1641
+ }
1642
+ };
1643
+ state.actionsBlocked++;
1644
+ store.save(state);
1645
+ process.stdout.write(JSON.stringify(denyJson));
1646
+ process.stderr.write(feedbackText);
1647
+ return 2;
1648
+ }
1649
+ }
1650
+ } catch (err) {
1651
+ console.error(`[ulpi] Plan review error: ${err instanceof Error ? err.message : err}`);
1652
+ }
1653
+ }
1654
+ return 0;
1655
+ }
1656
+
1657
+ // src/hooks/notification.ts
1658
+ async function handleNotification(ctx) {
1659
+ const { input, state, rules, store, projectDir: projectDir2 } = ctx;
1660
+ try {
1661
+ const result = await routeNotification(input, rules.responses);
1662
+ const actionCtx = {
1663
+ eventType: result.classified,
1664
+ state,
1665
+ projectDir: projectDir2
1666
+ };
1667
+ const actionResult = executeActions(result.actConfig, actionCtx);
1668
+ if (actionResult.stderrMessage) {
1669
+ process.stderr.write(actionResult.stderrMessage);
1670
+ }
1671
+ const channelsInfo = result.channels.length > 0 ? ` via ${result.channels.join(", ")}` : "";
1672
+ const suppressedInfo = result.suppressed ? " (suppressed by dedup)" : "";
1673
+ const actionsInfo = result.actions.length > 0 ? ` | actions: ${result.actions.join(", ")}` : "";
1674
+ appendEvent(input.session_id, {
1675
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1676
+ event: "notification_routed",
1677
+ hookEvent: input.hook_event_name,
1678
+ message: `Notification "${result.classified}"${channelsInfo}${suppressedInfo}${actionsInfo}`
1679
+ }, projectDir2);
1680
+ const auditEntry = {
1681
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1682
+ sessionId: input.session_id,
1683
+ eventType: result.classified,
1684
+ title: input.title ?? "ULPI",
1685
+ message: input.message ?? result.classified,
1686
+ channels: result.channels,
1687
+ actions: result.actions,
1688
+ suppressed: result.suppressed || actionResult.rateLimitSuppressed,
1689
+ suppressReason: result.suppressed ? "dedup" : actionResult.rateLimitSuppressed ? "rate_limit" : void 0
1690
+ };
1691
+ auditNotification(auditEntry, projectDir2);
1692
+ state.lastNotifications[result.classified] = Date.now();
1693
+ } catch {
1694
+ }
1695
+ store.save(state);
1696
+ }
1697
+
1698
+ // src/hooks/stop.ts
1699
+ async function handleStop(ctx) {
1700
+ const { input, state, rules, store, projectDir: projectDir2 } = ctx;
1701
+ const warnings = [];
1702
+ if (state.filesWritten.length > 0 && !state.testsRun) {
1703
+ warnings.push("Tests were not run during this session.");
1704
+ }
1705
+ if (state.filesWritten.length > 0 && !state.lintRun) {
1706
+ warnings.push("Linting was not run during this session.");
1707
+ }
1708
+ try {
1709
+ const triggers = evaluateNotificationTriggers(state, {
1710
+ type: "stop_requested",
1711
+ message: `Stop requested. Files written: ${state.filesWritten.length}`
1712
+ }, rules.responses);
1713
+ const notifyResult = await fireWithActions(triggers, rules.responses, {
1714
+ sessionId: input.session_id,
1715
+ state,
1716
+ projectDir: projectDir2,
1717
+ stderrMessage: warnings.length > 0 ? warnings.join("\n") : void 0
1718
+ });
1719
+ if (notifyResult.stderrMessage && warnings.length > 0) {
1720
+ warnings.push(notifyResult.stderrMessage);
1721
+ }
1722
+ for (const trigger of triggers) {
1723
+ appendEvent(input.session_id, {
1724
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1725
+ event: "notification_routed",
1726
+ hookEvent: input.hook_event_name,
1727
+ message: `Notification: ${trigger.eventType} -- ${trigger.message}`
1728
+ }, projectDir2);
1729
+ }
1730
+ } catch {
1731
+ }
1732
+ appendEvent(input.session_id, {
1733
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1734
+ event: "stop_requested",
1735
+ hookEvent: input.hook_event_name,
1736
+ message: `Stop requested. Files written: ${state.filesWritten.length}, Commands run: ${state.commandsRun.length}`
1737
+ }, projectDir2);
1738
+ store.save(state);
1739
+ if (warnings.length > 0 && input.stop_hook_active) {
1740
+ process.stderr.write(
1741
+ `[ulpi] Session warnings:
1742
+ ${warnings.map((w) => ` - ${w}`).join("\n")}`
1743
+ );
1744
+ return 2;
1745
+ }
1746
+ return 0;
1747
+ }
1748
+
1749
+ // src/hooks/session-end.ts
1750
+ async function captureSessionCommits(ctx) {
1751
+ const { input, state, projectDir: projectDir2 } = ctx;
1752
+ if (!state.headAtStart) return;
1753
+ if (!historyBranchExists(projectDir2)) return;
1754
+ const currentHead = getCurrentHead(projectDir2);
1755
+ if (!currentHead || currentHead === state.headAtStart) return;
1756
+ const newShas = listCommitsBetween(projectDir2, state.headAtStart, currentHead);
1757
+ if (newShas.length === 0) return;
1758
+ const events = readEvents(input.session_id, projectDir2);
1759
+ const sessionSummary = buildSessionSummary(state, events);
1760
+ const guardsYaml = loadActiveGuards(projectDir2);
1761
+ let transcriptContent = null;
1762
+ const meta = readBranchMeta(projectDir2);
1763
+ if (meta?.config?.captureTranscript !== false && input.transcript_path) {
1764
+ const maxSize = meta?.config?.maxTranscriptSize ?? 5242880;
1765
+ const result = readTranscript(input.transcript_path, maxSize);
1766
+ if (result) {
1767
+ transcriptContent = result.content;
1768
+ }
1769
+ }
1770
+ const collectReviewPlans = meta?.config?.collectReviewPlans ?? false;
1771
+ for (const sha of newShas) {
1772
+ try {
1773
+ if (entryExists(projectDir2, sha)) {
1774
+ if (transcriptContent && !readEntryTranscript(projectDir2, sha)) {
1775
+ await updateEntryTranscript(projectDir2, sha, transcriptContent);
1776
+ }
1777
+ continue;
1778
+ }
1779
+ const commit = getCommitMetadata(projectDir2, sha);
1780
+ const diff = getCommitDiffStats(projectDir2, sha);
1781
+ const { diff: rawDiff, truncated } = getCommitRawDiff(projectDir2, sha);
1782
+ let reviewPlanData = null;
1783
+ if (collectReviewPlans) {
1784
+ reviewPlanData = findReviewPlansForCommit(
1785
+ commit.authorDate,
1786
+ state.startedAt,
1787
+ projectDir2
1788
+ );
1789
+ }
1790
+ const entry = {
1791
+ version: 1,
1792
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1793
+ commit,
1794
+ diff,
1795
+ rawDiff: rawDiff || void 0,
1796
+ diffTruncated: truncated || void 0,
1797
+ session: sessionSummary,
1798
+ enrichment: null,
1799
+ reviewPlans: reviewPlanData?.snapshots ?? null,
1800
+ prePromptSnapshot: buildPrePromptSnapshot(state)
1801
+ };
1802
+ await writeHistoryEntry(projectDir2, entry, {
1803
+ state,
1804
+ events,
1805
+ guardsYaml,
1806
+ reviewPlans: reviewPlanData?.rawData,
1807
+ transcript: transcriptContent
1808
+ });
1809
+ } catch {
1810
+ continue;
1811
+ }
1812
+ }
1813
+ }
1814
+ async function handleSessionEnd(ctx) {
1815
+ const { input, state, store, projectDir: projectDir2 } = ctx;
1816
+ state.phase = "ended";
1817
+ appendEvent(input.session_id, {
1818
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1819
+ event: "session_end",
1820
+ hookEvent: input.hook_event_name,
1821
+ message: [
1822
+ `Session ended.`,
1823
+ `Files read: ${state.filesRead.length}`,
1824
+ `Files written: ${state.filesWritten.length}`,
1825
+ `Commands run: ${state.commandsRun.length}`,
1826
+ `Rules enforced: ${state.rulesEnforced}`,
1827
+ `Actions blocked: ${state.actionsBlocked}`,
1828
+ `Auto-actions run: ${state.autoActionsRun}`
1829
+ ].join(", ")
1830
+ }, projectDir2);
1831
+ store.save(state);
1832
+ try {
1833
+ await captureSessionCommits(ctx);
1834
+ } catch {
1835
+ }
1836
+ try {
1837
+ const memEngine = await import("./dist-R5F4MX3I.js");
1838
+ if (memEngine.isMemoryEnabled(projectDir2)) {
1839
+ const config = memEngine.loadMemoryConfig(projectDir2);
1840
+ memEngine.finalizeCapture(input.session_id, state, projectDir2);
1841
+ if (config.classifier.enabled) {
1842
+ await memEngine.classifySession(projectDir2, input.session_id);
1843
+ }
1844
+ if (config.autoExport) {
1845
+ try {
1846
+ memEngine.exportMemories(projectDir2);
1847
+ } catch {
1848
+ }
1849
+ }
1850
+ }
1851
+ } catch {
1852
+ }
1853
+ try {
1854
+ const { loadCodemapConfig, getCodemapStatus, exportIndex } = await import("./dist-LZKZFPVX.js");
1855
+ const { getCurrentBranch } = await import("./dist-RKOGLK7R.js");
1856
+ const codemapConfig = loadCodemapConfig(projectDir2);
1857
+ if (codemapConfig.autoExport) {
1858
+ const branch = state.branch ?? getCurrentBranch(projectDir2);
1859
+ const codemapStatus = getCodemapStatus(projectDir2, branch);
1860
+ if (codemapStatus.initialized && state.filesWritten.length > 0) {
1861
+ await exportIndex(projectDir2, branch);
1862
+ }
1863
+ }
1864
+ } catch {
1865
+ }
1866
+ }
1867
+
1868
+ // src/hooks/handler.ts
1869
+ function readStdinSync() {
1870
+ const raw = fs3.readFileSync(0, "utf-8");
1871
+ return JSON.parse(raw);
1872
+ }
1873
+ function findRulesPath(projectDir2) {
1874
+ const projectRules = projectGuardsFile(projectDir2);
1875
+ if (fs3.existsSync(projectRules)) return projectRules;
1876
+ const projectRulesYaml = projectGuardsFileAlt(projectDir2);
1877
+ if (fs3.existsSync(projectRulesYaml)) return projectRulesYaml;
1878
+ const userRules = globalGuardsFile();
1879
+ if (fs3.existsSync(userRules)) return userRules;
1880
+ return null;
1881
+ }
1882
+ function loadRules(projectDir2) {
1883
+ const rulesPath = findRulesPath(projectDir2);
1884
+ if (rulesPath) {
1885
+ const result = loadRulesSync(rulesPath);
1886
+ if (result) return result;
1887
+ }
1888
+ return {
1889
+ project: {
1890
+ name: path3.basename(projectDir2),
1891
+ runtime: "unknown",
1892
+ package_manager: "npm"
1893
+ },
1894
+ preconditions: {},
1895
+ postconditions: {},
1896
+ permissions: {},
1897
+ pipelines: {}
1898
+ };
1899
+ }
1900
+ function validateInput(input) {
1901
+ if (!input || typeof input !== "object") {
1902
+ throw new Error("Invalid hook input");
1903
+ }
1904
+ if (!input.session_id || typeof input.session_id !== "string") {
1905
+ throw new Error("Missing or invalid session_id");
1906
+ }
1907
+ if (input.session_id.length > 200) {
1908
+ throw new Error("session_id too long");
1909
+ }
1910
+ if (input.session_id.includes("..") || input.session_id.includes("/") || input.session_id.includes("\\")) {
1911
+ throw new Error("Invalid session_id characters");
1912
+ }
1913
+ if (!input.cwd || typeof input.cwd !== "string") {
1914
+ throw new Error("Missing or invalid cwd");
1915
+ }
1916
+ if (!input.hook_event_name || typeof input.hook_event_name !== "string") {
1917
+ throw new Error("Missing or invalid hook_event_name");
1918
+ }
1919
+ }
1920
+ async function handleHook(hookName) {
1921
+ const input = readStdinSync();
1922
+ validateInput(input);
1923
+ const projectDir2 = input.cwd;
1924
+ const store = new JsonSessionStore(void 0, projectDir2);
1925
+ let state = store.load(input.session_id);
1926
+ if (!state) {
1927
+ state = createInitialState(input.session_id, projectDir2);
1928
+ }
1929
+ if (input.title && input.title !== state.sessionName) {
1930
+ state.sessionName = input.title;
1931
+ }
1932
+ const rules = loadRules(projectDir2);
1933
+ const ctx = {
1934
+ input,
1935
+ state,
1936
+ rules,
1937
+ projectDir: projectDir2,
1938
+ store
1939
+ };
1940
+ if (hookName === "session-start") {
1941
+ try {
1942
+ registerProject(projectDir2, {
1943
+ sessionId: input.session_id
1944
+ });
1945
+ } catch {
1946
+ }
1947
+ }
1948
+ let exitCode = 0;
1949
+ switch (hookName) {
1950
+ case "session-start":
1951
+ await handleSessionStart(ctx);
1952
+ break;
1953
+ case "pre-tool":
1954
+ exitCode = await handlePreTool(ctx);
1955
+ break;
1956
+ case "post-tool":
1957
+ exitCode = await handlePostTool(ctx);
1958
+ break;
1959
+ case "permission":
1960
+ exitCode = await handlePermission(ctx);
1961
+ break;
1962
+ case "notification":
1963
+ await handleNotification(ctx);
1964
+ break;
1965
+ case "stop":
1966
+ exitCode = await handleStop(ctx);
1967
+ break;
1968
+ case "session-end":
1969
+ await handleSessionEnd(ctx);
1970
+ break;
1971
+ }
1972
+ process.exit(exitCode);
1973
+ }
1974
+
1975
+ // src/index.ts
1976
+ var rawArgs = process.argv.slice(2);
1977
+ var hookCommands = [
1978
+ "session-start",
1979
+ "pre-tool",
1980
+ "post-tool",
1981
+ "permission",
1982
+ "notification",
1983
+ "stop",
1984
+ "session-end"
1985
+ ];
1986
+ function parseGlobalFlags(args2) {
1987
+ const projectIdx = args2.findIndex((arg) => arg === "--project" || arg === "-p");
1988
+ if (projectIdx !== -1 && args2[projectIdx + 1]) {
1989
+ const projectRef = args2[projectIdx + 1];
1990
+ const entry = getProject(projectRef);
1991
+ if (!entry) {
1992
+ console.error(`Error: Unknown project: ${projectRef}`);
1993
+ console.error("Run 'ulpi projects list' to see registered projects.");
1994
+ process.exit(1);
1995
+ }
1996
+ const remaining = [...args2.slice(0, projectIdx), ...args2.slice(projectIdx + 2)];
1997
+ return { projectDir: entry.path, remainingArgs: remaining };
1998
+ }
1999
+ const cwd = process.cwd();
2000
+ const isProjectDir = fs4.existsSync(path4.join(cwd, ".ulpi"));
2001
+ if (isProjectDir) {
2002
+ return { projectDir: cwd, remainingArgs: args2 };
2003
+ }
2004
+ const defaultProject = getDefaultProject();
2005
+ if (defaultProject) {
2006
+ return { projectDir: defaultProject.path, remainingArgs: args2 };
2007
+ }
2008
+ return { projectDir: cwd, remainingArgs: args2 };
2009
+ }
2010
+ var { projectDir, remainingArgs } = parseGlobalFlags(rawArgs);
2011
+ var args = remainingArgs;
2012
+ var command = args[0];
2013
+ async function main() {
2014
+ if (!command) {
2015
+ printUsage();
2016
+ process.exit(0);
2017
+ }
2018
+ if (hookCommands.includes(command)) {
2019
+ await handleHook(command);
2020
+ return;
2021
+ }
2022
+ switch (command) {
2023
+ case "init": {
2024
+ const initDir = rawArgs.some((a) => a === "--project" || a === "-p") ? projectDir : process.cwd();
2025
+ return (await import("./init-AY5C2ZAS.js")).runInit([initDir, ...args.slice(1)]);
2026
+ }
2027
+ case "rules":
2028
+ return (await import("./rules-E427DKYJ.js")).runRules(args.slice(1), projectDir);
2029
+ case "templates":
2030
+ return (await import("./templates-U7T6MARD.js")).runTemplates(args.slice(1), projectDir);
2031
+ case "skills":
2032
+ return (await import("./skills-CX73O3IV.js")).runSkills(args.slice(1), projectDir);
2033
+ case "status":
2034
+ return (await import("./status-4DFHDJMN.js")).runStatus(args.slice(1), projectDir);
2035
+ case "log":
2036
+ return (await import("./log-TVTUXAYD.js")).runLog(args.slice(1), projectDir);
2037
+ case "export":
2038
+ return (await import("./export-import-4A5MWLIA.js")).runExport(args.slice(1), projectDir);
2039
+ case "import":
2040
+ return (await import("./export-import-4A5MWLIA.js")).runImport(args.slice(1), projectDir);
2041
+ case "uninstall":
2042
+ return (await import("./uninstall-6SW35IK4.js")).runUninstall(args.slice(1), projectDir);
2043
+ case "ui":
2044
+ return (await import("./ui-L7UAWXDY.js")).runUI(args.slice(1), projectDir);
2045
+ case "update":
2046
+ return (await import("./update-M2B4RLGH.js")).runUpdate(args.slice(1));
2047
+ case "history":
2048
+ return (await import("./history-ATTUKOHO.js")).runHistory(args.slice(1), projectDir);
2049
+ case "review":
2050
+ return (await import("./review-ADUPV3PN.js")).runReview(args.slice(1), projectDir);
2051
+ case "config":
2052
+ return (await import("./config-EGAXXCGL.js")).runConfig(args.slice(1));
2053
+ case "codemap":
2054
+ return (await import("./codemap-RRJIDBQ5.js")).runCodemap(args.slice(1), projectDir);
2055
+ case "memory":
2056
+ return (await import("./memory-J3G24QHS.js")).runMemory(args.slice(1), projectDir);
2057
+ case "projects":
2058
+ return (await import("./projects-ATHDD3D6.js")).runProjects(args.slice(1));
2059
+ case "--version":
2060
+ case "-v":
2061
+ console.log("0.1.0");
2062
+ return;
2063
+ case "--help":
2064
+ case "-h":
2065
+ printUsage();
2066
+ return;
2067
+ default:
2068
+ console.error(`Unknown command: ${command}`);
2069
+ printUsage();
2070
+ process.exit(1);
2071
+ }
2072
+ }
2073
+ function printUsage() {
2074
+ console.log(`
2075
+ ULPI \u2014 Rules engine for AI coding agents
2076
+
2077
+ Usage: ulpi <command> [options]
2078
+
2079
+ Hook Handlers (invoked by Claude Code):
2080
+ session-start Initialize session state
2081
+ pre-tool Evaluate rules before tool execution
2082
+ post-tool Track state and run postconditions
2083
+ permission Auto-approve or deny permissions
2084
+ notification Route notifications
2085
+ stop Final checks before stopping
2086
+ session-end Cleanup and persist summary
2087
+
2088
+ CLI Commands:
2089
+ init [--no-ai] [--model=<id>] Detect stack, generate guards, install hooks
2090
+ projects Manage registered projects (list/add/remove/default)
2091
+ rules Manage rules (list/add/enable/disable/validate)
2092
+ templates Manage templates (list/save/apply/delete)
2093
+ skills Manage skills (list/add/get/attach)
2094
+ status Show current session state
2095
+ log View activity log
2096
+ export Export rules configuration
2097
+ import Import rules configuration
2098
+ uninstall Remove hooks
2099
+ ui Start web UI
2100
+ history Shadow branch history (init/capture/list/show/enrich/backfill)
2101
+ review Plan and code review management (list/show/config/migrate)
2102
+ config Manage settings and API keys
2103
+ codemap Semantic code indexing (init/search/status/reindex/watch)
2104
+ memory Agent memory (init/search/remember/status/export/import/serve)
2105
+ update Check for and install updates
2106
+
2107
+ Global Options:
2108
+ -p, --project <id|path> Target a specific registered project
2109
+ -v, --version Show version
2110
+ -h, --help Show this help
2111
+ `.trim());
2112
+ }
2113
+ main().catch((err) => {
2114
+ if (hookCommands.includes(command)) {
2115
+ console.error(`[ulpi] Error: ${err instanceof Error ? err.message : String(err)}`);
2116
+ process.exit(0);
2117
+ }
2118
+ console.error(err instanceof Error ? err.message : String(err));
2119
+ process.exit(1);
2120
+ });