@samboyd/bep-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.
package/dist/cli.js ADDED
@@ -0,0 +1,3349 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ main: () => main
35
+ });
36
+ module.exports = __toCommonJS(cli_exports);
37
+ var import_commander = require("commander");
38
+
39
+ // src/fs/init.ts
40
+ var import_promises2 = require("fs/promises");
41
+ var import_node_path2 = __toESM(require("path"));
42
+
43
+ // src/providers/config.ts
44
+ var import_promises = require("fs/promises");
45
+ var import_node_path = __toESM(require("path"));
46
+ var PROVIDER_CONFIG_PATH = ".bep.providers.json";
47
+ async function readProviderConfig(rootDir) {
48
+ const configPath = import_node_path.default.join(rootDir, PROVIDER_CONFIG_PATH);
49
+ let raw;
50
+ try {
51
+ raw = await (0, import_promises.readFile)(configPath, "utf8");
52
+ } catch (error) {
53
+ if (error.code === "ENOENT") {
54
+ return {
55
+ ok: false,
56
+ error: `Missing provider config at ${PROVIDER_CONFIG_PATH}. Run 'bep init' to scaffold it.`
57
+ };
58
+ }
59
+ return {
60
+ ok: false,
61
+ error: `Failed to read provider config at ${PROVIDER_CONFIG_PATH}: ${error.message}`
62
+ };
63
+ }
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(raw);
67
+ } catch (error) {
68
+ return {
69
+ ok: false,
70
+ error: `Invalid JSON in ${PROVIDER_CONFIG_PATH}: ${error.message}`
71
+ };
72
+ }
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
74
+ return { ok: false, error: `${PROVIDER_CONFIG_PATH} must contain a JSON object.` };
75
+ }
76
+ const config = parsed;
77
+ return { ok: true, value: config };
78
+ }
79
+ function getMixpanelServiceAccountCreds(config) {
80
+ if (!config.mixpanel || typeof config.mixpanel !== "object") {
81
+ return {
82
+ ok: false,
83
+ error: `Missing "mixpanel" object in ${PROVIDER_CONFIG_PATH}.`
84
+ };
85
+ }
86
+ if (typeof config.mixpanel.service_account_creds !== "string" || config.mixpanel.service_account_creds.trim().length === 0) {
87
+ return {
88
+ ok: false,
89
+ error: `Missing non-empty "mixpanel.service_account_creds" in ${PROVIDER_CONFIG_PATH}.`
90
+ };
91
+ }
92
+ const creds = config.mixpanel.service_account_creds.trim();
93
+ const colonIndex = creds.indexOf(":");
94
+ if (colonIndex <= 0 || colonIndex !== creds.lastIndexOf(":") || colonIndex === creds.length - 1) {
95
+ return {
96
+ ok: false,
97
+ error: `Invalid "mixpanel.service_account_creds" in ${PROVIDER_CONFIG_PATH}. Expected "<serviceaccount_username>:<serviceaccount_secret>".`
98
+ };
99
+ }
100
+ return { ok: true, value: creds };
101
+ }
102
+
103
+ // src/fs/init.ts
104
+ var BETS_DIR = "bets";
105
+ var LOGS_DIR = import_node_path2.default.join(BETS_DIR, "_logs");
106
+ var EVIDENCE_DIR = import_node_path2.default.join(BETS_DIR, "_evidence");
107
+ var STATE_PATH = import_node_path2.default.join(BETS_DIR, "_state.json");
108
+ var GITIGNORE_PATH = ".gitignore";
109
+ var PROVIDER_GITIGNORE_ENTRY = ".bep.providers.json";
110
+ var DEFAULT_STATE = {
111
+ active: []
112
+ };
113
+ var DEFAULT_PROVIDER_CONFIG = {
114
+ mixpanel: {
115
+ service_account_creds: "<serviceaccount_username>:<serviceaccount_secret>"
116
+ }
117
+ };
118
+ var REQUIRED_INIT_PATHS = [BETS_DIR, LOGS_DIR, EVIDENCE_DIR, STATE_PATH];
119
+ async function pathExists(filePath) {
120
+ try {
121
+ await (0, import_promises2.access)(filePath);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+ async function initRepo(rootDir) {
128
+ const createdPaths = [];
129
+ for (const relativeDir of [BETS_DIR, LOGS_DIR, EVIDENCE_DIR]) {
130
+ const absoluteDir = import_node_path2.default.join(rootDir, relativeDir);
131
+ const existed = await pathExists(absoluteDir);
132
+ await (0, import_promises2.mkdir)(absoluteDir, { recursive: true });
133
+ if (!existed) {
134
+ createdPaths.push(relativeDir);
135
+ }
136
+ }
137
+ const statePath = import_node_path2.default.join(rootDir, STATE_PATH);
138
+ const stateExists = await pathExists(statePath);
139
+ if (!stateExists) {
140
+ await (0, import_promises2.writeFile)(statePath, `${JSON.stringify(DEFAULT_STATE, null, 2)}
141
+ `, "utf8");
142
+ createdPaths.push(STATE_PATH);
143
+ }
144
+ const providerConfigPath = import_node_path2.default.join(rootDir, PROVIDER_CONFIG_PATH);
145
+ const providerConfigExists = await pathExists(providerConfigPath);
146
+ if (!providerConfigExists) {
147
+ await (0, import_promises2.writeFile)(providerConfigPath, `${JSON.stringify(DEFAULT_PROVIDER_CONFIG, null, 2)}
148
+ `, "utf8");
149
+ createdPaths.push(PROVIDER_CONFIG_PATH);
150
+ }
151
+ const gitRoot = await findGitRepoRoot(rootDir);
152
+ if (gitRoot) {
153
+ await ensureGitignoreEntry(gitRoot, createdPaths);
154
+ }
155
+ return {
156
+ createdPaths,
157
+ alreadyInitialized: createdPaths.length === 0
158
+ };
159
+ }
160
+ async function findGitRepoRoot(startDir) {
161
+ let currentDir = import_node_path2.default.resolve(startDir);
162
+ while (true) {
163
+ if (await pathExists(import_node_path2.default.join(currentDir, ".git"))) {
164
+ return currentDir;
165
+ }
166
+ const parentDir = import_node_path2.default.dirname(currentDir);
167
+ if (parentDir === currentDir) {
168
+ return null;
169
+ }
170
+ currentDir = parentDir;
171
+ }
172
+ }
173
+ async function ensureGitignoreEntry(gitRoot, createdPaths) {
174
+ const gitignorePath = import_node_path2.default.join(gitRoot, GITIGNORE_PATH);
175
+ const exists = await pathExists(gitignorePath);
176
+ if (!exists) {
177
+ await (0, import_promises2.writeFile)(gitignorePath, `${PROVIDER_GITIGNORE_ENTRY}
178
+ `, "utf8");
179
+ createdPaths.push(GITIGNORE_PATH);
180
+ return;
181
+ }
182
+ const raw = await (0, import_promises2.readFile)(gitignorePath, "utf8");
183
+ const lines = raw.split(/\r?\n/);
184
+ if (lines.includes(PROVIDER_GITIGNORE_ENTRY)) {
185
+ return;
186
+ }
187
+ const suffix = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
188
+ await (0, import_promises2.writeFile)(gitignorePath, `${raw}${suffix}${PROVIDER_GITIGNORE_ENTRY}
189
+ `, "utf8");
190
+ }
191
+ async function isInitializedRepoRoot(candidateRootDir) {
192
+ for (const relativePath of REQUIRED_INIT_PATHS) {
193
+ const absolutePath = import_node_path2.default.join(candidateRootDir, relativePath);
194
+ if (!await pathExists(absolutePath)) {
195
+ return false;
196
+ }
197
+ }
198
+ return true;
199
+ }
200
+ async function findInitializedRepo(startDir) {
201
+ let currentDir = import_node_path2.default.resolve(startDir);
202
+ while (true) {
203
+ if (await isInitializedRepoRoot(currentDir)) {
204
+ return {
205
+ rootDir: currentDir,
206
+ betsDir: import_node_path2.default.join(currentDir, BETS_DIR)
207
+ };
208
+ }
209
+ const parentDir = import_node_path2.default.dirname(currentDir);
210
+ if (parentDir === currentDir) {
211
+ return null;
212
+ }
213
+ currentDir = parentDir;
214
+ }
215
+ }
216
+ async function ensureInitializedRepo(startDir) {
217
+ const found = await findInitializedRepo(startDir);
218
+ if (!found) {
219
+ throw new Error("fatal: not a bep repository (or any of the parent directories): bets");
220
+ }
221
+ return found;
222
+ }
223
+
224
+ // src/hooks/install.ts
225
+ var import_node_path5 = __toESM(require("path"));
226
+
227
+ // src/hooks/discovery.ts
228
+ var import_promises3 = require("fs/promises");
229
+ var import_node_path3 = __toESM(require("path"));
230
+ async function findNearestClaudeDir(startDir) {
231
+ let currentDir = import_node_path3.default.resolve(startDir);
232
+ while (true) {
233
+ const candidate = import_node_path3.default.join(currentDir, ".claude");
234
+ try {
235
+ const stats = await (0, import_promises3.stat)(candidate);
236
+ if (stats.isDirectory()) {
237
+ return candidate;
238
+ }
239
+ } catch {
240
+ }
241
+ const parentDir = import_node_path3.default.dirname(currentDir);
242
+ if (parentDir === currentDir) {
243
+ return null;
244
+ }
245
+ currentDir = parentDir;
246
+ }
247
+ }
248
+
249
+ // src/hooks/claude.ts
250
+ var import_promises4 = require("fs/promises");
251
+ var import_node_path4 = __toESM(require("path"));
252
+ var CLAUDE_SETTINGS_FILE = "settings.json";
253
+ var HOOK_EVENTS = [
254
+ { event: "UserPromptSubmit", suffix: "user-prompt-submit" },
255
+ { event: "PostToolUse", suffix: "post-tool-use" },
256
+ { event: "PostToolUseFailure", suffix: "post-tool-use-failure" },
257
+ { event: "SessionEnd", suffix: "session-end" }
258
+ ];
259
+ function isObject(value) {
260
+ return value !== null && typeof value === "object";
261
+ }
262
+ function parseSettings(raw) {
263
+ const parsed = JSON.parse(raw);
264
+ if (!isObject(parsed)) {
265
+ throw new Error(".claude/settings.json must be a JSON object.");
266
+ }
267
+ return parsed;
268
+ }
269
+ function ensureCommand(settings, event, command) {
270
+ if (!isObject(settings.hooks)) {
271
+ settings.hooks = {};
272
+ }
273
+ const hooksByEvent = settings.hooks;
274
+ const rawMatchers = hooksByEvent[event];
275
+ if (!Array.isArray(rawMatchers)) {
276
+ const entry = {
277
+ matcher: "",
278
+ hooks: [{ type: "command", command }]
279
+ };
280
+ hooksByEvent[event] = [entry];
281
+ return true;
282
+ }
283
+ let target = rawMatchers.find(
284
+ (matcher) => isObject(matcher) && typeof matcher.matcher === "string" && matcher.matcher.length === 0 && Array.isArray(matcher.hooks)
285
+ );
286
+ if (!target) {
287
+ target = { matcher: "", hooks: [] };
288
+ rawMatchers.push(target);
289
+ }
290
+ const exists = target.hooks.some(
291
+ (candidate) => candidate && candidate.type === "command" && candidate.command === command
292
+ );
293
+ if (exists) {
294
+ return false;
295
+ }
296
+ target.hooks.push({ type: "command", command });
297
+ return true;
298
+ }
299
+ async function installClaudeCodeHooks(claudeDir, hookCommandBase) {
300
+ const settingsPath = import_node_path4.default.join(claudeDir, CLAUDE_SETTINGS_FILE);
301
+ let settings = {};
302
+ try {
303
+ const raw = await (0, import_promises4.readFile)(settingsPath, "utf8");
304
+ settings = parseSettings(raw);
305
+ } catch (error) {
306
+ const code = error.code;
307
+ if (code !== "ENOENT") {
308
+ throw error;
309
+ }
310
+ }
311
+ let addedCommands = 0;
312
+ for (const hook of HOOK_EVENTS) {
313
+ const command = `${hookCommandBase} hook claude-code ${hook.suffix}`;
314
+ if (ensureCommand(settings, hook.event, command)) {
315
+ addedCommands += 1;
316
+ }
317
+ }
318
+ await (0, import_promises4.writeFile)(settingsPath, `${JSON.stringify(settings, null, 2)}
319
+ `, "utf8");
320
+ return {
321
+ claudeDir,
322
+ settingsPath,
323
+ addedCommands
324
+ };
325
+ }
326
+
327
+ // src/hooks/types.ts
328
+ var SUPPORTED_HOOK_AGENT = "claude-code";
329
+ var AGENT_CHOICES = ["claude-code", "cursor", "codex", "windsurf"];
330
+ function isHookAgent(value) {
331
+ return AGENT_CHOICES.includes(value);
332
+ }
333
+ function isSupportedHookAgent(value) {
334
+ return value === SUPPORTED_HOOK_AGENT;
335
+ }
336
+ function formatAgentLabel(agent) {
337
+ switch (agent) {
338
+ case "claude-code":
339
+ return "Claude Code";
340
+ case "cursor":
341
+ return "Cursor";
342
+ case "codex":
343
+ return "Codex";
344
+ case "windsurf":
345
+ return "Windsurf";
346
+ }
347
+ }
348
+
349
+ // src/hooks/install.ts
350
+ function resolveAgent(agent) {
351
+ if (!isHookAgent(agent)) {
352
+ return {
353
+ ok: false,
354
+ error: `Unknown agent '${agent}'. Valid values: claude-code, cursor, codex, windsurf.`
355
+ };
356
+ }
357
+ if (!isSupportedHookAgent(agent)) {
358
+ return {
359
+ ok: false,
360
+ error: `${formatAgentLabel(agent)} hook installation is not supported yet. Choose 'claude-code'.`
361
+ };
362
+ }
363
+ return { ok: true, value: agent };
364
+ }
365
+ function quoteShellArg(value) {
366
+ if (/^[A-Za-z0-9_./:@-]+$/.test(value)) {
367
+ return value;
368
+ }
369
+ return `'${value.replace(/'/g, `'\\''`)}'`;
370
+ }
371
+ function resolveHookCommandBase(startDir) {
372
+ const argv1 = process.argv[1]?.trim();
373
+ if (!argv1) {
374
+ return "bep";
375
+ }
376
+ const resolved = import_node_path5.default.isAbsolute(argv1) ? argv1 : import_node_path5.default.resolve(startDir, argv1);
377
+ return quoteShellArg(resolved);
378
+ }
379
+ async function installAgentHooks(startDir, agent) {
380
+ const resolved = resolveAgent(agent);
381
+ if (!resolved.ok) {
382
+ return resolved;
383
+ }
384
+ const claudeDir = await findNearestClaudeDir(startDir);
385
+ if (!claudeDir) {
386
+ return {
387
+ ok: false,
388
+ error: "No .claude directory found in the current directory or any parent directory. Create one first, then rerun 'bep init --install-hooks --agent claude-code'."
389
+ };
390
+ }
391
+ const hookCommandBase = resolveHookCommandBase(startDir);
392
+ const installed = await installClaudeCodeHooks(claudeDir, hookCommandBase);
393
+ const settingsPathRelative = import_node_path5.default.relative(startDir, installed.settingsPath) || import_node_path5.default.basename(installed.settingsPath);
394
+ return {
395
+ ok: true,
396
+ agent: resolved.value,
397
+ settingsPathRelative,
398
+ alreadyInstalled: installed.addedCommands === 0
399
+ };
400
+ }
401
+
402
+ // src/ui/initHooks.ts
403
+ var import_prompts = require("@clack/prompts");
404
+ var COMING_SOON_AGENTS = ["cursor", "codex", "windsurf"];
405
+ async function runInitHookPrompt(client = createInitHookPromptClient()) {
406
+ const shouldInstall = await client.promptInstallNow();
407
+ if (shouldInstall === "cancel") {
408
+ return { kind: "cancel" };
409
+ }
410
+ if (!shouldInstall) {
411
+ return { kind: "skip" };
412
+ }
413
+ while (true) {
414
+ const selected = await client.promptAgent();
415
+ if (selected === "cancel") {
416
+ return { kind: "cancel" };
417
+ }
418
+ if (selected === "claude-code") {
419
+ return { kind: "install", agent: selected };
420
+ }
421
+ client.showComingSoon(selected);
422
+ }
423
+ }
424
+ function createInitHookPromptClient() {
425
+ return {
426
+ async promptInstallNow() {
427
+ const value = await (0, import_prompts.confirm)({
428
+ message: "Install agent tracking hooks now?",
429
+ initialValue: true
430
+ });
431
+ if ((0, import_prompts.isCancel)(value)) {
432
+ return "cancel";
433
+ }
434
+ return value;
435
+ },
436
+ async promptAgent() {
437
+ const value = await (0, import_prompts.select)({
438
+ message: "Choose an agent",
439
+ options: [
440
+ { label: "Claude Code", value: "claude-code" },
441
+ { label: "Cursor", value: "cursor", hint: "coming soon" },
442
+ { label: "Codex", value: "codex", hint: "coming soon" },
443
+ { label: "Windsurf", value: "windsurf", hint: "coming soon" }
444
+ ],
445
+ initialValue: "claude-code"
446
+ });
447
+ if ((0, import_prompts.isCancel)(value)) {
448
+ return "cancel";
449
+ }
450
+ return value;
451
+ },
452
+ showComingSoon(agent) {
453
+ if (COMING_SOON_AGENTS.includes(agent)) {
454
+ console.log(`${formatAgentLabel(agent)} support is coming soon. Choose Claude Code for now.`);
455
+ }
456
+ }
457
+ };
458
+ }
459
+
460
+ // src/commands/init.ts
461
+ function isInteractiveTty() {
462
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
463
+ }
464
+ async function runInit(options = {}) {
465
+ const cwd = process.cwd();
466
+ const result = await initRepo(cwd);
467
+ if (result.alreadyInitialized) {
468
+ console.log("BEP is already initialized.");
469
+ } else {
470
+ console.log(`Initialized BEP in this repository (${result.createdPaths.length} items created).`);
471
+ }
472
+ let shouldInstallHooks = options.installHooks;
473
+ if (shouldInstallHooks === void 0 && options.agent !== void 0) {
474
+ shouldInstallHooks = true;
475
+ }
476
+ if (shouldInstallHooks === false && options.agent) {
477
+ console.error("Cannot use --agent with --no-install-hooks.");
478
+ return 1;
479
+ }
480
+ if (shouldInstallHooks === false) {
481
+ return 0;
482
+ }
483
+ let selectedAgent = options.agent;
484
+ if (shouldInstallHooks === void 0 && isInteractiveTty()) {
485
+ const promptResult = await runInitHookPrompt();
486
+ if (promptResult.kind === "cancel") {
487
+ console.error("Cancelled hook setup.");
488
+ return 1;
489
+ }
490
+ if (promptResult.kind === "skip") {
491
+ return 0;
492
+ }
493
+ selectedAgent = promptResult.agent;
494
+ }
495
+ if (shouldInstallHooks === void 0 && !selectedAgent) {
496
+ return 0;
497
+ }
498
+ const installResult = await installAgentHooks(cwd, selectedAgent ?? "claude-code");
499
+ if (!installResult.ok) {
500
+ console.error(installResult.error);
501
+ return 1;
502
+ }
503
+ if (installResult.alreadyInstalled) {
504
+ console.log(`Claude Code tracking hooks are already installed (${installResult.settingsPathRelative}).`);
505
+ return 0;
506
+ }
507
+ console.log(`Installed Claude Code tracking hooks in ${installResult.settingsPathRelative}.`);
508
+ return 0;
509
+ }
510
+
511
+ // src/commands/check.ts
512
+ var import_promises6 = require("fs/promises");
513
+ var import_node_path7 = __toESM(require("path"));
514
+ var import_prompts5 = require("@clack/prompts");
515
+
516
+ // src/bep/id.ts
517
+ var BET_ID_REGEX = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
518
+ function isValidBetId(id) {
519
+ return BET_ID_REGEX.test(id);
520
+ }
521
+
522
+ // src/bep/status.ts
523
+ function normalizeValidationStatus(value) {
524
+ return value === "passed" ? "passed" : "pending";
525
+ }
526
+
527
+ // src/fs/bets.ts
528
+ var import_promises5 = require("fs/promises");
529
+ var import_node_path6 = __toESM(require("path"));
530
+ var import_gray_matter = __toESM(require("gray-matter"));
531
+ function getBetRelativePath(idOrFileName) {
532
+ const fileName = idOrFileName.endsWith(".md") ? idOrFileName : `${idOrFileName}.md`;
533
+ return import_node_path6.default.join(BETS_DIR, fileName);
534
+ }
535
+ function getBetAbsolutePath(rootDir, idOrFileName) {
536
+ return import_node_path6.default.join(rootDir, getBetRelativePath(idOrFileName));
537
+ }
538
+ async function pathExists2(filePath) {
539
+ try {
540
+ await (0, import_promises5.access)(filePath);
541
+ return true;
542
+ } catch {
543
+ return false;
544
+ }
545
+ }
546
+ async function readBetFile(rootDir, idOrFileName) {
547
+ const relativePath = getBetRelativePath(idOrFileName);
548
+ const absolutePath = getBetAbsolutePath(rootDir, idOrFileName);
549
+ let markdown;
550
+ try {
551
+ markdown = await (0, import_promises5.readFile)(absolutePath, "utf8");
552
+ } catch (error) {
553
+ throw new Error(`Failed to parse BEP file at ${relativePath}: ${error.message}`);
554
+ }
555
+ let parsed;
556
+ try {
557
+ parsed = (0, import_gray_matter.default)(markdown);
558
+ } catch (error) {
559
+ throw new Error(`Failed to parse BEP file at ${relativePath}: ${error.message}`);
560
+ }
561
+ return {
562
+ relativePath,
563
+ absolutePath,
564
+ markdown,
565
+ bet: {
566
+ content: parsed.content,
567
+ data: parsed.data
568
+ }
569
+ };
570
+ }
571
+ async function listBetMarkdownFiles(rootDir) {
572
+ const betsDir = import_node_path6.default.join(rootDir, BETS_DIR);
573
+ const entries = await (0, import_promises5.readdir)(betsDir, { withFileTypes: true });
574
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && !entry.name.startsWith("_")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
575
+ }
576
+ async function writeBetFile(rootDir, idOrFileName, bet) {
577
+ const absolutePath = getBetAbsolutePath(rootDir, idOrFileName);
578
+ await (0, import_promises5.writeFile)(absolutePath, import_gray_matter.default.stringify(bet.content, bet.data), "utf8");
579
+ }
580
+
581
+ // src/providers/manual.ts
582
+ var import_prompts3 = require("@clack/prompts");
583
+
584
+ // src/ui/checkPrompt.ts
585
+ var import_prompts2 = require("@clack/prompts");
586
+ function createClackCheckPromptClient() {
587
+ return {
588
+ async promptObservedValue() {
589
+ return (0, import_prompts2.text)({
590
+ message: "Observed value (required, numeric)",
591
+ validate(rawValue) {
592
+ const trimmed = rawValue.trim();
593
+ if (trimmed.length === 0 || !Number.isFinite(Number(trimmed))) {
594
+ return "Enter a valid number.";
595
+ }
596
+ }
597
+ });
598
+ },
599
+ async promptNotes() {
600
+ return (0, import_prompts2.text)({
601
+ message: "Notes (optional)"
602
+ });
603
+ }
604
+ };
605
+ }
606
+ async function runCheckPrompt(client = createClackCheckPromptClient()) {
607
+ const observed = await client.promptObservedValue();
608
+ if ((0, import_prompts2.isCancel)(observed)) {
609
+ return { cancelled: true };
610
+ }
611
+ const notes = await client.promptNotes();
612
+ if ((0, import_prompts2.isCancel)(notes)) {
613
+ return { cancelled: true };
614
+ }
615
+ const observedValue = Number(observed.trim());
616
+ const trimmedNotes = (notes || "").trim();
617
+ return {
618
+ cancelled: false,
619
+ observedValue,
620
+ notes: trimmedNotes.length > 0 ? trimmedNotes : void 0
621
+ };
622
+ }
623
+
624
+ // src/providers/manual.ts
625
+ var BACK_VALUE = "__back__";
626
+ var DIM = "\x1B[2m";
627
+ var RESET = "\x1B[0m";
628
+ function isManualComparisonOperator(value) {
629
+ return value === "lt" || value === "lte" || value === "eq" || value === "gte" || value === "gt";
630
+ }
631
+ function parseManualLeadingIndicator(input) {
632
+ if (!input || typeof input !== "object") {
633
+ return { ok: false, error: "leading_indicator must be an object." };
634
+ }
635
+ const candidate = input;
636
+ if (candidate.type !== "manual") {
637
+ return { ok: false, error: 'leading_indicator.type must equal "manual".' };
638
+ }
639
+ if (!isManualComparisonOperator(candidate.operator)) {
640
+ return { ok: false, error: 'leading_indicator.operator must be one of "lt", "lte", "eq", "gte", "gt".' };
641
+ }
642
+ if (typeof candidate.target !== "number" || !Number.isFinite(candidate.target)) {
643
+ return { ok: false, error: "leading_indicator.target must be a finite number." };
644
+ }
645
+ return {
646
+ ok: true,
647
+ value: {
648
+ type: "manual",
649
+ operator: candidate.operator,
650
+ target: candidate.target
651
+ }
652
+ };
653
+ }
654
+ function evaluateManualComparison(observedValue, operator, target) {
655
+ if (operator === "lt") {
656
+ return observedValue < target;
657
+ }
658
+ if (operator === "lte") {
659
+ return observedValue <= target;
660
+ }
661
+ if (operator === "eq") {
662
+ return observedValue === target;
663
+ }
664
+ if (operator === "gte") {
665
+ return observedValue >= target;
666
+ }
667
+ return observedValue > target;
668
+ }
669
+ function formatManualComparisonOperator(operator) {
670
+ if (operator === "lt") {
671
+ return "<";
672
+ }
673
+ if (operator === "lte") {
674
+ return "<=";
675
+ }
676
+ if (operator === "eq") {
677
+ return "=";
678
+ }
679
+ if (operator === "gte") {
680
+ return ">=";
681
+ }
682
+ return ">";
683
+ }
684
+ function createClackManualSetupPromptClient() {
685
+ return {
686
+ async promptManualOperator({ initialValue, allowBack }) {
687
+ const options = [
688
+ { label: "lt (less than)", value: "lt" },
689
+ { label: "lte (less than or equal)", value: "lte" },
690
+ { label: "eq (equal)", value: "eq" },
691
+ { label: "gte (greater than or equal)", value: "gte" },
692
+ { label: "gt (greater than)", value: "gt" }
693
+ ];
694
+ if (allowBack) {
695
+ options.unshift({ label: "Back", value: BACK_VALUE });
696
+ }
697
+ const value = await (0, import_prompts3.select)({
698
+ message: "Leading indicator comparison operator",
699
+ options,
700
+ initialValue
701
+ });
702
+ if ((0, import_prompts3.isCancel)(value)) {
703
+ return { kind: "cancel" };
704
+ }
705
+ if (value === BACK_VALUE) {
706
+ return { kind: "back" };
707
+ }
708
+ return { kind: "value", value };
709
+ },
710
+ async promptManualTarget({ initialValue, allowBack }) {
711
+ const backHint = allowBack ? ` ${DIM}(type b to go back)${RESET}` : "";
712
+ const value = await (0, import_prompts3.text)({
713
+ message: `Leading indicator numeric target (required).${backHint}`,
714
+ initialValue: typeof initialValue === "number" ? String(initialValue) : void 0,
715
+ validate(rawValue) {
716
+ const trimmed2 = rawValue.trim();
717
+ if (allowBack && trimmed2.toLowerCase() === "b") {
718
+ return;
719
+ }
720
+ if (trimmed2.length === 0 || !Number.isFinite(Number(trimmed2))) {
721
+ return "Enter a valid number.";
722
+ }
723
+ }
724
+ });
725
+ if ((0, import_prompts3.isCancel)(value)) {
726
+ return { kind: "cancel" };
727
+ }
728
+ const trimmed = value.trim();
729
+ if (allowBack && trimmed.toLowerCase() === "b") {
730
+ return { kind: "back" };
731
+ }
732
+ return { kind: "value", value: Number(trimmed) };
733
+ }
734
+ };
735
+ }
736
+ var manualAdapter = {
737
+ type: "manual",
738
+ parseIndicator(input) {
739
+ return parseManualLeadingIndicator(input);
740
+ },
741
+ async runCheck(indicator) {
742
+ const promptResult = await runCheckPrompt();
743
+ if (promptResult.cancelled) {
744
+ return { cancelled: true };
745
+ }
746
+ return {
747
+ observedValue: promptResult.observedValue,
748
+ meetsTarget: evaluateManualComparison(promptResult.observedValue, indicator.operator, indicator.target),
749
+ notes: promptResult.notes
750
+ };
751
+ }
752
+ };
753
+ function getManualSetupClient(ctx) {
754
+ if (ctx.client && typeof ctx.client === "object") {
755
+ const candidate = ctx.client;
756
+ if (typeof candidate.promptManualOperator === "function" && typeof candidate.promptManualTarget === "function") {
757
+ return candidate;
758
+ }
759
+ }
760
+ return createClackManualSetupPromptClient();
761
+ }
762
+ var manualSetup = {
763
+ type: "manual",
764
+ async collectNewWizardInput(ctx) {
765
+ const client = getManualSetupClient(ctx);
766
+ let stepIndex = 0;
767
+ const values = {
768
+ type: "manual",
769
+ operator: ctx.initialValue?.operator,
770
+ target: ctx.initialValue?.target
771
+ };
772
+ while (stepIndex < 2) {
773
+ if (stepIndex === 0) {
774
+ const result2 = await client.promptManualOperator({
775
+ initialValue: values.operator,
776
+ allowBack: ctx.allowBack
777
+ });
778
+ if (result2.kind === "cancel") {
779
+ return { kind: "cancel" };
780
+ }
781
+ if (result2.kind === "back") {
782
+ return { kind: "back" };
783
+ }
784
+ values.operator = result2.value;
785
+ stepIndex += 1;
786
+ continue;
787
+ }
788
+ const result = await client.promptManualTarget({
789
+ initialValue: values.target,
790
+ allowBack: true
791
+ });
792
+ if (result.kind === "cancel") {
793
+ return { kind: "cancel" };
794
+ }
795
+ if (result.kind === "back") {
796
+ stepIndex = Math.max(0, stepIndex - 1);
797
+ continue;
798
+ }
799
+ values.target = result.value;
800
+ stepIndex += 1;
801
+ }
802
+ if (!values.operator || typeof values.target !== "number") {
803
+ return { kind: "cancel" };
804
+ }
805
+ return {
806
+ kind: "value",
807
+ value: {
808
+ type: "manual",
809
+ operator: values.operator,
810
+ target: values.target
811
+ }
812
+ };
813
+ }
814
+ };
815
+ var manualProviderModule = {
816
+ adapter: manualAdapter,
817
+ setup: manualSetup
818
+ };
819
+
820
+ // src/providers/mixpanel.ts
821
+ var import_node_buffer = require("buffer");
822
+ var import_prompts4 = require("@clack/prompts");
823
+ var BACK_VALUE2 = "__back__";
824
+ var DIM2 = "\x1B[2m";
825
+ var RESET2 = "\x1B[0m";
826
+ var MIXPANEL_REPORT_ENDPOINT = "https://mixpanel.com/api/query/insights";
827
+ var MIXPANEL_URL_HINT = "Values come from report URL: /project/<PROJECT_ID>/view/<WORKSPACE_ID>/...#...report-<BOOKMARK_ID>.";
828
+ function isManualComparisonOperator2(value) {
829
+ return value === "lt" || value === "lte" || value === "eq" || value === "gte" || value === "gt";
830
+ }
831
+ function parseMixpanelLeadingIndicator(input) {
832
+ if (!input || typeof input !== "object") {
833
+ return { ok: false, error: "leading_indicator must be an object." };
834
+ }
835
+ const candidate = input;
836
+ if (candidate.type !== "mixpanel") {
837
+ return { ok: false, error: 'leading_indicator.type must equal "mixpanel".' };
838
+ }
839
+ if (typeof candidate.project_id !== "string" || candidate.project_id.trim().length === 0) {
840
+ return { ok: false, error: "leading_indicator.project_id must be a non-empty string." };
841
+ }
842
+ if (typeof candidate.workspace_id !== "string" || candidate.workspace_id.trim().length === 0) {
843
+ return { ok: false, error: "leading_indicator.workspace_id must be a non-empty string." };
844
+ }
845
+ if (typeof candidate.bookmark_id !== "string" || candidate.bookmark_id.trim().length === 0) {
846
+ return { ok: false, error: "leading_indicator.bookmark_id must be a non-empty string." };
847
+ }
848
+ if (!isManualComparisonOperator2(candidate.operator)) {
849
+ return { ok: false, error: 'leading_indicator.operator must be one of "lt", "lte", "eq", "gte", "gt".' };
850
+ }
851
+ if (typeof candidate.target !== "number" || !Number.isFinite(candidate.target)) {
852
+ return { ok: false, error: "leading_indicator.target must be a finite number." };
853
+ }
854
+ return {
855
+ ok: true,
856
+ value: {
857
+ type: "mixpanel",
858
+ project_id: candidate.project_id.trim(),
859
+ workspace_id: candidate.workspace_id.trim(),
860
+ bookmark_id: candidate.bookmark_id.trim(),
861
+ operator: candidate.operator,
862
+ target: candidate.target
863
+ }
864
+ };
865
+ }
866
+ function getMixpanelSetupClient(ctx) {
867
+ if (ctx.client && typeof ctx.client === "object") {
868
+ const candidate = ctx.client;
869
+ if (typeof candidate.promptMixpanelProjectId === "function" && typeof candidate.promptMixpanelWorkspaceId === "function" && typeof candidate.promptMixpanelBookmarkId === "function" && typeof candidate.promptMixpanelOperator === "function" && typeof candidate.promptMixpanelTarget === "function") {
870
+ return candidate;
871
+ }
872
+ }
873
+ return createClackMixpanelSetupPromptClient();
874
+ }
875
+ function createClackMixpanelSetupPromptClient() {
876
+ return {
877
+ async promptMixpanelProjectId({ initialValue, allowBack }) {
878
+ const backHint = allowBack ? ` ${DIM2}(type b to go back)${RESET2}` : "";
879
+ const value = await (0, import_prompts4.text)({
880
+ message: `Mixpanel project id (required). ${MIXPANEL_URL_HINT}${backHint}`,
881
+ initialValue,
882
+ validate(rawValue) {
883
+ const trimmed2 = rawValue.trim();
884
+ if (allowBack && trimmed2.toLowerCase() === "b") {
885
+ return;
886
+ }
887
+ if (trimmed2.length === 0) {
888
+ return "Enter a project id.";
889
+ }
890
+ }
891
+ });
892
+ if ((0, import_prompts4.isCancel)(value)) {
893
+ return { kind: "cancel" };
894
+ }
895
+ const trimmed = value.trim();
896
+ if (allowBack && trimmed.toLowerCase() === "b") {
897
+ return { kind: "back" };
898
+ }
899
+ return { kind: "value", value: trimmed };
900
+ },
901
+ async promptMixpanelWorkspaceId({ initialValue, allowBack }) {
902
+ const backHint = allowBack ? ` ${DIM2}(type b to go back)${RESET2}` : "";
903
+ const value = await (0, import_prompts4.text)({
904
+ message: `Mixpanel workspace id (required). ${MIXPANEL_URL_HINT}${backHint}`,
905
+ initialValue,
906
+ validate(rawValue) {
907
+ const trimmed2 = rawValue.trim();
908
+ if (allowBack && trimmed2.toLowerCase() === "b") {
909
+ return;
910
+ }
911
+ if (trimmed2.length === 0) {
912
+ return "Enter a workspace id.";
913
+ }
914
+ }
915
+ });
916
+ if ((0, import_prompts4.isCancel)(value)) {
917
+ return { kind: "cancel" };
918
+ }
919
+ const trimmed = value.trim();
920
+ if (allowBack && trimmed.toLowerCase() === "b") {
921
+ return { kind: "back" };
922
+ }
923
+ return { kind: "value", value: trimmed };
924
+ },
925
+ async promptMixpanelBookmarkId({ initialValue, allowBack }) {
926
+ const backHint = allowBack ? ` ${DIM2}(type b to go back)${RESET2}` : "";
927
+ const value = await (0, import_prompts4.text)({
928
+ message: `Mixpanel bookmark id (required). ${MIXPANEL_URL_HINT}${backHint}`,
929
+ initialValue,
930
+ validate(rawValue) {
931
+ const trimmed2 = rawValue.trim();
932
+ if (allowBack && trimmed2.toLowerCase() === "b") {
933
+ return;
934
+ }
935
+ if (trimmed2.length === 0) {
936
+ return "Enter a bookmark id.";
937
+ }
938
+ }
939
+ });
940
+ if ((0, import_prompts4.isCancel)(value)) {
941
+ return { kind: "cancel" };
942
+ }
943
+ const trimmed = value.trim();
944
+ if (allowBack && trimmed.toLowerCase() === "b") {
945
+ return { kind: "back" };
946
+ }
947
+ return { kind: "value", value: trimmed };
948
+ },
949
+ async promptMixpanelOperator({ initialValue, allowBack }) {
950
+ const options = [
951
+ { label: "lt (less than)", value: "lt" },
952
+ { label: "lte (less than or equal)", value: "lte" },
953
+ { label: "eq (equal)", value: "eq" },
954
+ { label: "gte (greater than or equal)", value: "gte" },
955
+ { label: "gt (greater than)", value: "gt" }
956
+ ];
957
+ if (allowBack) {
958
+ options.unshift({ label: "Back", value: BACK_VALUE2 });
959
+ }
960
+ const value = await (0, import_prompts4.select)({
961
+ message: "Mixpanel comparison operator",
962
+ options,
963
+ initialValue
964
+ });
965
+ if ((0, import_prompts4.isCancel)(value)) {
966
+ return { kind: "cancel" };
967
+ }
968
+ if (value === BACK_VALUE2) {
969
+ return { kind: "back" };
970
+ }
971
+ return { kind: "value", value };
972
+ },
973
+ async promptMixpanelTarget({ initialValue, allowBack }) {
974
+ const backHint = allowBack ? ` ${DIM2}(type b to go back)${RESET2}` : "";
975
+ const value = await (0, import_prompts4.text)({
976
+ message: `Mixpanel target value (required).${backHint}`,
977
+ initialValue: typeof initialValue === "number" ? String(initialValue) : void 0,
978
+ validate(rawValue) {
979
+ const trimmed2 = rawValue.trim();
980
+ if (allowBack && trimmed2.toLowerCase() === "b") {
981
+ return;
982
+ }
983
+ if (trimmed2.length === 0 || !Number.isFinite(Number(trimmed2))) {
984
+ return "Enter a valid number.";
985
+ }
986
+ }
987
+ });
988
+ if ((0, import_prompts4.isCancel)(value)) {
989
+ return { kind: "cancel" };
990
+ }
991
+ const trimmed = value.trim();
992
+ if (allowBack && trimmed.toLowerCase() === "b") {
993
+ return { kind: "back" };
994
+ }
995
+ return { kind: "value", value: Number(trimmed) };
996
+ }
997
+ };
998
+ }
999
+ function collectNumericLeaves(value) {
1000
+ const numbers = [];
1001
+ const stack = [value];
1002
+ while (stack.length > 0) {
1003
+ const current = stack.pop();
1004
+ if (typeof current === "number" && Number.isFinite(current)) {
1005
+ numbers.push(current);
1006
+ continue;
1007
+ }
1008
+ if (!current || typeof current !== "object") {
1009
+ continue;
1010
+ }
1011
+ for (const nested of Object.values(current)) {
1012
+ stack.push(nested);
1013
+ }
1014
+ }
1015
+ return numbers;
1016
+ }
1017
+ function parseObservedValue(payload) {
1018
+ if (!payload || typeof payload !== "object") {
1019
+ return { value: null, seriesNumericLeafCount: null };
1020
+ }
1021
+ const candidate = payload;
1022
+ if (typeof candidate.value === "number" && Number.isFinite(candidate.value)) {
1023
+ return { value: candidate.value, seriesNumericLeafCount: null };
1024
+ }
1025
+ if (candidate.result && typeof candidate.result === "object") {
1026
+ const maybeResult = candidate.result;
1027
+ if (typeof maybeResult.value === "number" && Number.isFinite(maybeResult.value)) {
1028
+ return { value: maybeResult.value, seriesNumericLeafCount: null };
1029
+ }
1030
+ }
1031
+ if (candidate.series && typeof candidate.series === "object") {
1032
+ const numericLeaves = collectNumericLeaves(candidate.series);
1033
+ if (numericLeaves.length === 1) {
1034
+ return { value: numericLeaves[0], seriesNumericLeafCount: 1 };
1035
+ }
1036
+ return {
1037
+ value: null,
1038
+ seriesNumericLeafCount: numericLeaves.length
1039
+ };
1040
+ }
1041
+ return { value: null, seriesNumericLeafCount: null };
1042
+ }
1043
+ var mixpanelAdapter = {
1044
+ type: "mixpanel",
1045
+ parseIndicator(input) {
1046
+ return parseMixpanelLeadingIndicator(input);
1047
+ },
1048
+ async runCheck(indicator, ctx) {
1049
+ const configResult = await readProviderConfig(ctx.rootDir);
1050
+ if (!configResult.ok) {
1051
+ throw new Error(configResult.error);
1052
+ }
1053
+ const credsResult = getMixpanelServiceAccountCreds(configResult.value);
1054
+ if (!credsResult.ok) {
1055
+ throw new Error(credsResult.error);
1056
+ }
1057
+ const params = new URLSearchParams({
1058
+ project_id: indicator.project_id,
1059
+ workspace_id: indicator.workspace_id,
1060
+ bookmark_id: indicator.bookmark_id
1061
+ });
1062
+ const url = `${MIXPANEL_REPORT_ENDPOINT}?${params.toString()}`;
1063
+ const encodedCreds = import_node_buffer.Buffer.from(credsResult.value, "utf8").toString("base64");
1064
+ let response;
1065
+ try {
1066
+ response = await fetch(url, {
1067
+ headers: {
1068
+ authorization: `Basic ${encodedCreds}`,
1069
+ accept: "application/json"
1070
+ }
1071
+ });
1072
+ } catch (error) {
1073
+ throw new Error(`Failed to query Mixpanel insights API: ${error.message}`);
1074
+ }
1075
+ if (!response.ok) {
1076
+ throw new Error(`Mixpanel insights API returned ${response.status} ${response.statusText}.`);
1077
+ }
1078
+ let payload;
1079
+ try {
1080
+ payload = await response.json();
1081
+ } catch (error) {
1082
+ throw new Error(`Failed to parse Mixpanel response JSON: ${error.message}`);
1083
+ }
1084
+ const observed = parseObservedValue(payload);
1085
+ if (observed.value === null) {
1086
+ if (observed.seriesNumericLeafCount !== null) {
1087
+ throw new Error(
1088
+ observed.seriesNumericLeafCount === 0 ? "Mixpanel response series must contain exactly one numeric value; found 0." : `Mixpanel response series must contain exactly one numeric value; found ${observed.seriesNumericLeafCount}. Use a single-value insight/report.`
1089
+ );
1090
+ }
1091
+ throw new Error("Mixpanel response did not include a numeric value field.");
1092
+ }
1093
+ const observedValue = observed.value;
1094
+ return {
1095
+ observedValue,
1096
+ meetsTarget: evaluateManualComparison(observedValue, indicator.operator, indicator.target),
1097
+ meta: {
1098
+ provider: "mixpanel",
1099
+ project_id: indicator.project_id,
1100
+ workspace_id: indicator.workspace_id,
1101
+ bookmark_id: indicator.bookmark_id
1102
+ }
1103
+ };
1104
+ }
1105
+ };
1106
+ var mixpanelSetup = {
1107
+ type: "mixpanel",
1108
+ async collectNewWizardInput(ctx) {
1109
+ const client = getMixpanelSetupClient(ctx);
1110
+ let stepIndex = 0;
1111
+ const values = {
1112
+ type: "mixpanel",
1113
+ project_id: ctx.initialValue?.project_id,
1114
+ workspace_id: ctx.initialValue?.workspace_id,
1115
+ bookmark_id: ctx.initialValue?.bookmark_id,
1116
+ operator: ctx.initialValue?.operator,
1117
+ target: ctx.initialValue?.target
1118
+ };
1119
+ while (stepIndex < 5) {
1120
+ if (stepIndex === 0) {
1121
+ const result2 = await client.promptMixpanelProjectId({
1122
+ initialValue: values.project_id,
1123
+ allowBack: ctx.allowBack
1124
+ });
1125
+ if (result2.kind === "cancel") {
1126
+ return { kind: "cancel" };
1127
+ }
1128
+ if (result2.kind === "back") {
1129
+ return { kind: "back" };
1130
+ }
1131
+ values.project_id = result2.value;
1132
+ stepIndex += 1;
1133
+ continue;
1134
+ }
1135
+ if (stepIndex === 1) {
1136
+ const result2 = await client.promptMixpanelWorkspaceId({
1137
+ initialValue: values.workspace_id,
1138
+ allowBack: true
1139
+ });
1140
+ if (result2.kind === "cancel") {
1141
+ return { kind: "cancel" };
1142
+ }
1143
+ if (result2.kind === "back") {
1144
+ stepIndex = Math.max(0, stepIndex - 1);
1145
+ continue;
1146
+ }
1147
+ values.workspace_id = result2.value;
1148
+ stepIndex += 1;
1149
+ continue;
1150
+ }
1151
+ if (stepIndex === 2) {
1152
+ const result2 = await client.promptMixpanelBookmarkId({
1153
+ initialValue: values.bookmark_id,
1154
+ allowBack: true
1155
+ });
1156
+ if (result2.kind === "cancel") {
1157
+ return { kind: "cancel" };
1158
+ }
1159
+ if (result2.kind === "back") {
1160
+ stepIndex = Math.max(0, stepIndex - 1);
1161
+ continue;
1162
+ }
1163
+ values.bookmark_id = result2.value;
1164
+ stepIndex += 1;
1165
+ continue;
1166
+ }
1167
+ if (stepIndex === 3) {
1168
+ const result2 = await client.promptMixpanelOperator({
1169
+ initialValue: values.operator,
1170
+ allowBack: true
1171
+ });
1172
+ if (result2.kind === "cancel") {
1173
+ return { kind: "cancel" };
1174
+ }
1175
+ if (result2.kind === "back") {
1176
+ stepIndex = Math.max(0, stepIndex - 1);
1177
+ continue;
1178
+ }
1179
+ values.operator = result2.value;
1180
+ stepIndex += 1;
1181
+ continue;
1182
+ }
1183
+ const result = await client.promptMixpanelTarget({
1184
+ initialValue: values.target,
1185
+ allowBack: true
1186
+ });
1187
+ if (result.kind === "cancel") {
1188
+ return { kind: "cancel" };
1189
+ }
1190
+ if (result.kind === "back") {
1191
+ stepIndex = Math.max(0, stepIndex - 1);
1192
+ continue;
1193
+ }
1194
+ values.target = result.value;
1195
+ stepIndex += 1;
1196
+ }
1197
+ if (!values.project_id || !values.workspace_id || !values.bookmark_id || !values.operator || typeof values.target !== "number") {
1198
+ return { kind: "cancel" };
1199
+ }
1200
+ return {
1201
+ kind: "value",
1202
+ value: {
1203
+ type: "mixpanel",
1204
+ project_id: values.project_id,
1205
+ workspace_id: values.workspace_id,
1206
+ bookmark_id: values.bookmark_id,
1207
+ operator: values.operator,
1208
+ target: values.target
1209
+ }
1210
+ };
1211
+ }
1212
+ };
1213
+ var mixpanelProviderModule = {
1214
+ adapter: mixpanelAdapter,
1215
+ setup: mixpanelSetup
1216
+ };
1217
+
1218
+ // src/providers/registry.ts
1219
+ var providerRegistry = {
1220
+ manual: manualProviderModule,
1221
+ mixpanel: mixpanelProviderModule
1222
+ };
1223
+ function resolveProviderModule(type) {
1224
+ return providerRegistry[type];
1225
+ }
1226
+ function listRegisteredProviderTypes() {
1227
+ return Object.keys(providerRegistry);
1228
+ }
1229
+
1230
+ // src/commands/check.ts
1231
+ function getLeadingIndicatorType(value) {
1232
+ if (!value || typeof value !== "object") {
1233
+ return null;
1234
+ }
1235
+ const type = value.type;
1236
+ return typeof type === "string" && type.length > 0 ? type : null;
1237
+ }
1238
+ function formatComparisonLabel(indicator, observedValue) {
1239
+ if (indicator.type === "manual" || indicator.type === "mixpanel") {
1240
+ return `${observedValue} ${formatManualComparisonOperator(indicator.operator)} ${indicator.target}`;
1241
+ }
1242
+ return String(observedValue);
1243
+ }
1244
+ function hasPassedStatusInFrontmatter(markdown) {
1245
+ const trimmed = markdown.trimStart();
1246
+ if (!trimmed.startsWith("---")) {
1247
+ return false;
1248
+ }
1249
+ const lines = trimmed.split(/\r?\n/);
1250
+ if (lines.length < 3 || lines[0]?.trim() !== "---") {
1251
+ return false;
1252
+ }
1253
+ const endIndex = lines.slice(1).findIndex((line) => line.trim() === "---");
1254
+ if (endIndex === -1) {
1255
+ return false;
1256
+ }
1257
+ const frontmatterLines = lines.slice(1, endIndex + 1);
1258
+ return frontmatterLines.some((line) => /^status:\s*passed\s*$/i.test(line.trim()));
1259
+ }
1260
+ async function hasPassingEvidence(rootDir, id) {
1261
+ const evidencePath = import_node_path7.default.join(rootDir, EVIDENCE_DIR, `${id}.json`);
1262
+ if (!await pathExists2(evidencePath)) {
1263
+ return false;
1264
+ }
1265
+ let parsed;
1266
+ try {
1267
+ parsed = JSON.parse(await (0, import_promises6.readFile)(evidencePath, "utf8"));
1268
+ } catch {
1269
+ return false;
1270
+ }
1271
+ if (!parsed || typeof parsed !== "object") {
1272
+ return false;
1273
+ }
1274
+ return parsed.meets_target === true;
1275
+ }
1276
+ function isInteractiveTty2() {
1277
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1278
+ }
1279
+ async function maybePromptUnpass(id) {
1280
+ const value = await (0, import_prompts5.select)({
1281
+ message: `Bet '${id}' is currently status: passed, but the forced check FAILED. Update status?`,
1282
+ options: [
1283
+ { label: "Keep status: passed", value: "keep" },
1284
+ { label: "Set status: pending", value: "unpass" }
1285
+ ],
1286
+ initialValue: "keep"
1287
+ });
1288
+ if ((0, import_prompts5.isCancel)(value)) {
1289
+ return "cancel";
1290
+ }
1291
+ return value;
1292
+ }
1293
+ async function runCheck(id, options = {}) {
1294
+ if (!isValidBetId(id)) {
1295
+ console.error(`Invalid bet id '${id}'. Use lowercase id format like 'landing-page' or 'landing_page'.`);
1296
+ return 1;
1297
+ }
1298
+ let rootDir;
1299
+ try {
1300
+ if (options.rootDir) {
1301
+ const ensured = await ensureInitializedRepo(options.rootDir);
1302
+ if (ensured.rootDir !== options.rootDir) {
1303
+ throw new Error(`Expected BEP repo root at ${options.rootDir}, found ${ensured.rootDir}.`);
1304
+ }
1305
+ rootDir = ensured.rootDir;
1306
+ } else {
1307
+ const cwd = process.cwd();
1308
+ ({ rootDir } = await ensureInitializedRepo(cwd));
1309
+ }
1310
+ } catch (error) {
1311
+ console.error(error.message);
1312
+ return 1;
1313
+ }
1314
+ const relativeBetPath = getBetRelativePath(id);
1315
+ const absoluteBetPath = getBetAbsolutePath(rootDir, id);
1316
+ if (!await pathExists2(absoluteBetPath)) {
1317
+ console.error(`Bet '${id}' does not exist at ${relativeBetPath}. Run 'bep new ${id}' first.`);
1318
+ return 1;
1319
+ }
1320
+ let bet;
1321
+ try {
1322
+ bet = await readBetFile(rootDir, id);
1323
+ } catch (error) {
1324
+ console.error(error.message);
1325
+ return 1;
1326
+ }
1327
+ const validationStatus = normalizeValidationStatus(bet.bet.data.status);
1328
+ const isPassed = hasPassedStatusInFrontmatter(bet.markdown) || validationStatus === "passed";
1329
+ if (isPassed && !options.force) {
1330
+ if (await hasPassingEvidence(rootDir, id)) {
1331
+ console.log(`Bet '${id}' is status: passed; skipping validation check.`);
1332
+ return 0;
1333
+ }
1334
+ }
1335
+ const rawLeadingIndicator = bet.bet.data.leading_indicator;
1336
+ const leadingIndicatorType = getLeadingIndicatorType(rawLeadingIndicator);
1337
+ if (!leadingIndicatorType) {
1338
+ console.error("Bet has invalid leading_indicator: missing string field 'type'.");
1339
+ return 1;
1340
+ }
1341
+ const module2 = resolveProviderModule(leadingIndicatorType);
1342
+ if (!module2) {
1343
+ const knownTypes = listRegisteredProviderTypes().join(", ");
1344
+ console.error(
1345
+ `Bet has unsupported leading_indicator.type '${leadingIndicatorType}'. Supported types: ${knownTypes}.`
1346
+ );
1347
+ return 1;
1348
+ }
1349
+ const parsedIndicator = module2.adapter.parseIndicator(rawLeadingIndicator);
1350
+ if (!parsedIndicator.ok) {
1351
+ console.error(`Bet '${id}' has invalid leading_indicator: ${parsedIndicator.error}`);
1352
+ return 1;
1353
+ }
1354
+ let checkResult;
1355
+ try {
1356
+ checkResult = await module2.adapter.runCheck(parsedIndicator.value, {
1357
+ rootDir,
1358
+ betId: id,
1359
+ nowIso: (/* @__PURE__ */ new Date()).toISOString()
1360
+ });
1361
+ } catch (error) {
1362
+ console.error(error.message);
1363
+ return 1;
1364
+ }
1365
+ if ("cancelled" in checkResult) {
1366
+ console.error("Cancelled. No evidence was written.");
1367
+ return 1;
1368
+ }
1369
+ const snapshot = {
1370
+ id,
1371
+ checked_at: (/* @__PURE__ */ new Date()).toISOString(),
1372
+ mode: parsedIndicator.value.type,
1373
+ leading_indicator: parsedIndicator.value,
1374
+ observed_value: checkResult.observedValue,
1375
+ meets_target: checkResult.meetsTarget,
1376
+ notes: checkResult.notes,
1377
+ meta: checkResult.meta
1378
+ };
1379
+ const relativeEvidencePath = import_node_path7.default.join(EVIDENCE_DIR, `${id}.json`);
1380
+ const absoluteEvidencePath = import_node_path7.default.join(rootDir, relativeEvidencePath);
1381
+ try {
1382
+ await (0, import_promises6.writeFile)(absoluteEvidencePath, `${JSON.stringify(snapshot, null, 2)}
1383
+ `, "utf8");
1384
+ } catch (error) {
1385
+ console.error(`Failed to write evidence at ${relativeEvidencePath}: ${error.message}`);
1386
+ return 1;
1387
+ }
1388
+ const comparisonLabel = formatComparisonLabel(parsedIndicator.value, checkResult.observedValue);
1389
+ console.log(
1390
+ `Captured ${parsedIndicator.value.type} evidence for '${id}' at ${relativeEvidencePath}. Result: ${checkResult.meetsTarget ? "PASS" : "FAIL"} (${comparisonLabel}).`
1391
+ );
1392
+ if (checkResult.meetsTarget) {
1393
+ bet.bet.data.status = "passed";
1394
+ try {
1395
+ await writeBetFile(rootDir, id, bet.bet);
1396
+ } catch (error) {
1397
+ console.error(`Failed to mark bet '${id}' as passed: ${error.message}`);
1398
+ return 1;
1399
+ }
1400
+ console.log(`Marked bet '${id}' as status: passed.`);
1401
+ return 0;
1402
+ }
1403
+ if (options.force && isPassed) {
1404
+ if (!isInteractiveTty2()) {
1405
+ console.log(
1406
+ `Note: Bet '${id}' remains status: passed. To unpass, edit bets/${id}.md and set status: pending.`
1407
+ );
1408
+ return 0;
1409
+ }
1410
+ const result = await maybePromptUnpass(id);
1411
+ if (result === "cancel") {
1412
+ console.log(`Cancelled; bet '${id}' remains status: passed.`);
1413
+ return 0;
1414
+ }
1415
+ if (result === "unpass") {
1416
+ bet.bet.data.status = "pending";
1417
+ try {
1418
+ await writeBetFile(rootDir, id, bet.bet);
1419
+ } catch (error) {
1420
+ console.error(`Failed to update bet '${id}' status: ${error.message}`);
1421
+ return 1;
1422
+ }
1423
+ console.log(`Updated bet '${id}' to status: pending.`);
1424
+ }
1425
+ }
1426
+ return 0;
1427
+ }
1428
+
1429
+ // src/commands/new.ts
1430
+ var import_promises7 = require("fs/promises");
1431
+ var import_node_path8 = __toESM(require("path"));
1432
+
1433
+ // src/bep/template.ts
1434
+ var import_gray_matter2 = __toESM(require("gray-matter"));
1435
+ function renderNewBetMarkdown(input) {
1436
+ const frontmatter = {
1437
+ id: input.id,
1438
+ status: "pending",
1439
+ created_at: input.createdAt,
1440
+ leading_indicator: input.leadingIndicator
1441
+ };
1442
+ if (typeof input.maxHours === "number") {
1443
+ frontmatter.max_hours = input.maxHours;
1444
+ }
1445
+ if (typeof input.maxCalendarDays === "number") {
1446
+ frontmatter.max_calendar_days = input.maxCalendarDays;
1447
+ }
1448
+ const body = [
1449
+ "# Budgeted Engineering Proposal",
1450
+ "",
1451
+ "## 1. Primary Assumption",
1452
+ "",
1453
+ input.primaryAssumption,
1454
+ "",
1455
+ "## 2. Rationale",
1456
+ "",
1457
+ input.rationale,
1458
+ "",
1459
+ "## 3. Validation Plan",
1460
+ "",
1461
+ input.validationPlan,
1462
+ "",
1463
+ "## 4. Notes",
1464
+ "",
1465
+ input.notes,
1466
+ ""
1467
+ ].join("\n");
1468
+ return `${import_gray_matter2.default.stringify(body, frontmatter)}`;
1469
+ }
1470
+
1471
+ // src/ui/newWizard.ts
1472
+ var import_prompts6 = require("@clack/prompts");
1473
+ var BACK_VALUE3 = "__back__";
1474
+ var DIM3 = "\x1B[2m";
1475
+ var RESET3 = "\x1B[0m";
1476
+ var STEP_ORDER = [
1477
+ "cap_type",
1478
+ "cap_value",
1479
+ "leading_indicator_type",
1480
+ "leading_indicator_setup",
1481
+ "primary_assumption",
1482
+ "rationale",
1483
+ "validation_plan",
1484
+ "notes"
1485
+ ];
1486
+ function applyPromptResult(result, onValue) {
1487
+ if (result.kind === "cancel") {
1488
+ return { kind: "cancel" };
1489
+ }
1490
+ if (result.kind === "back") {
1491
+ return { kind: "back" };
1492
+ }
1493
+ onValue(result.value);
1494
+ return { kind: "next" };
1495
+ }
1496
+ function finalizeWizardValues(values) {
1497
+ if (!values.capType || typeof values.capValue !== "number" || !values.leadingIndicator || !values.primaryAssumption || !values.rationale || !values.validationPlan || values.notes === void 0) {
1498
+ return null;
1499
+ }
1500
+ const maxHours = values.capType === "max_hours" ? values.capValue : void 0;
1501
+ const maxCalendarDays = values.capType === "max_calendar_days" ? values.capValue : void 0;
1502
+ return {
1503
+ maxHours,
1504
+ maxCalendarDays,
1505
+ leadingIndicator: values.leadingIndicator,
1506
+ primaryAssumption: values.primaryAssumption,
1507
+ rationale: values.rationale,
1508
+ validationPlan: values.validationPlan,
1509
+ notes: values.notes
1510
+ };
1511
+ }
1512
+ var STEP_HANDLERS = {
1513
+ async cap_type({ client, values, stepIndex }) {
1514
+ const result = await client.promptCapType({
1515
+ initialValue: values.capType,
1516
+ allowBack: stepIndex > 0
1517
+ });
1518
+ return applyPromptResult(result, (value) => {
1519
+ const previousCapType = values.capType;
1520
+ values.capType = value;
1521
+ if (previousCapType !== value) {
1522
+ values.capValue = void 0;
1523
+ }
1524
+ });
1525
+ },
1526
+ async cap_value({ client, values, stepIndex }) {
1527
+ if (!values.capType) {
1528
+ return { kind: "cancel" };
1529
+ }
1530
+ const result = await client.promptCapValue({
1531
+ field: values.capType,
1532
+ initialValue: values.capValue,
1533
+ allowBack: stepIndex > 0
1534
+ });
1535
+ return applyPromptResult(result, (value) => {
1536
+ values.capValue = value;
1537
+ });
1538
+ },
1539
+ async leading_indicator_type({ client, values, stepIndex }) {
1540
+ const result = await client.promptLeadingIndicatorType({
1541
+ initialValue: values.leadingIndicatorType,
1542
+ allowBack: stepIndex > 0
1543
+ });
1544
+ return applyPromptResult(result, (value) => {
1545
+ if (values.leadingIndicatorType !== value) {
1546
+ values.leadingIndicator = void 0;
1547
+ }
1548
+ values.leadingIndicatorType = value;
1549
+ });
1550
+ },
1551
+ async leading_indicator_setup({ client, values, stepIndex }) {
1552
+ if (!values.leadingIndicatorType) {
1553
+ return { kind: "cancel" };
1554
+ }
1555
+ const module2 = resolveProviderModule(values.leadingIndicatorType);
1556
+ if (!module2 || !module2.setup) {
1557
+ return { kind: "cancel" };
1558
+ }
1559
+ const setupResult = await module2.setup.collectNewWizardInput({
1560
+ allowBack: stepIndex > 0,
1561
+ initialValue: values.leadingIndicator && values.leadingIndicator.type === values.leadingIndicatorType ? values.leadingIndicator : void 0,
1562
+ client
1563
+ });
1564
+ return applyPromptResult(setupResult, (value) => {
1565
+ values.leadingIndicator = value;
1566
+ });
1567
+ },
1568
+ async primary_assumption({ client, values, stepIndex }) {
1569
+ const result = await client.promptPrimaryAssumption({
1570
+ initialValue: values.primaryAssumption,
1571
+ allowBack: stepIndex > 0
1572
+ });
1573
+ return applyPromptResult(result, (value) => {
1574
+ values.primaryAssumption = value;
1575
+ });
1576
+ },
1577
+ async rationale({ client, values, stepIndex }) {
1578
+ const result = await client.promptRationale({
1579
+ initialValue: values.rationale,
1580
+ allowBack: stepIndex > 0
1581
+ });
1582
+ return applyPromptResult(result, (value) => {
1583
+ values.rationale = value;
1584
+ });
1585
+ },
1586
+ async validation_plan({ client, values, stepIndex }) {
1587
+ const result = await client.promptValidationPlan({
1588
+ initialValue: values.validationPlan,
1589
+ allowBack: stepIndex > 0
1590
+ });
1591
+ return applyPromptResult(result, (value) => {
1592
+ values.validationPlan = value;
1593
+ });
1594
+ },
1595
+ async notes({ client, values, stepIndex }) {
1596
+ const result = await client.promptNotes({
1597
+ initialValue: values.notes,
1598
+ allowBack: stepIndex > 0
1599
+ });
1600
+ return applyPromptResult(result, (value) => {
1601
+ values.notes = value;
1602
+ });
1603
+ }
1604
+ };
1605
+ async function runNewWizard(client = createClackPromptClient(), log = console.log) {
1606
+ let stepIndex = 0;
1607
+ const values = {};
1608
+ while (stepIndex < STEP_ORDER.length) {
1609
+ const step = STEP_ORDER[stepIndex];
1610
+ const handler = STEP_HANDLERS[step];
1611
+ if (!handler) {
1612
+ throw new Error(`No wizard step handler registered for '${step}'.`);
1613
+ }
1614
+ const flow = await handler({ client, values, stepIndex });
1615
+ if (flow.kind === "cancel") {
1616
+ return { cancelled: true };
1617
+ }
1618
+ if (flow.kind === "back") {
1619
+ stepIndex = Math.max(0, stepIndex - 1);
1620
+ continue;
1621
+ }
1622
+ stepIndex += 1;
1623
+ }
1624
+ const finalizedValues = finalizeWizardValues(values);
1625
+ if (!finalizedValues) {
1626
+ return { cancelled: true };
1627
+ }
1628
+ return {
1629
+ cancelled: false,
1630
+ values: finalizedValues
1631
+ };
1632
+ }
1633
+ function createClackPromptClient() {
1634
+ const mixpanelPromptClient = createClackMixpanelSetupPromptClient();
1635
+ return {
1636
+ async promptCapType({ initialValue, allowBack }) {
1637
+ const options = [
1638
+ { label: "Cap by hours", value: "max_hours" },
1639
+ { label: "Cap by calendar days", value: "max_calendar_days" }
1640
+ ];
1641
+ if (allowBack) {
1642
+ options.unshift({ label: "Back", value: BACK_VALUE3 });
1643
+ }
1644
+ const value = await (0, import_prompts6.select)({
1645
+ message: "Choose your exposure cap type",
1646
+ options,
1647
+ initialValue
1648
+ });
1649
+ if ((0, import_prompts6.isCancel)(value)) {
1650
+ return { kind: "cancel" };
1651
+ }
1652
+ if (value === BACK_VALUE3) {
1653
+ return { kind: "back" };
1654
+ }
1655
+ return { kind: "value", value };
1656
+ },
1657
+ async promptCapValue({ field, initialValue, allowBack }) {
1658
+ const label = field === "max_hours" ? "Max hours" : "Max calendar days";
1659
+ const backHint = allowBack ? ` ${DIM3}(type b to go back)${RESET3}` : "";
1660
+ const value = await (0, import_prompts6.text)({
1661
+ message: `${label} (required).${backHint}`,
1662
+ initialValue: typeof initialValue === "number" ? String(initialValue) : void 0,
1663
+ validate(rawValue) {
1664
+ const trimmed2 = rawValue.trim();
1665
+ if (allowBack && trimmed2.toLowerCase() === "b") {
1666
+ return;
1667
+ }
1668
+ if (trimmed2.length === 0) {
1669
+ return "Enter a positive number.";
1670
+ }
1671
+ const parsed = Number(trimmed2);
1672
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1673
+ return "Enter a positive number.";
1674
+ }
1675
+ }
1676
+ });
1677
+ if ((0, import_prompts6.isCancel)(value)) {
1678
+ return { kind: "cancel" };
1679
+ }
1680
+ const trimmed = value.trim();
1681
+ if (allowBack && trimmed.toLowerCase() === "b") {
1682
+ return { kind: "back" };
1683
+ }
1684
+ return { kind: "value", value: Number(trimmed) };
1685
+ },
1686
+ async promptLeadingIndicatorType({ initialValue, allowBack }) {
1687
+ const options = listRegisteredProviderTypes().map((type) => ({ label: type, value: type }));
1688
+ if (allowBack) {
1689
+ options.unshift({ label: "Back", value: BACK_VALUE3 });
1690
+ }
1691
+ const value = await (0, import_prompts6.select)({
1692
+ message: "Leading indicator provider type",
1693
+ options,
1694
+ initialValue
1695
+ });
1696
+ if ((0, import_prompts6.isCancel)(value)) {
1697
+ return { kind: "cancel" };
1698
+ }
1699
+ if (value === BACK_VALUE3) {
1700
+ return { kind: "back" };
1701
+ }
1702
+ return { kind: "value", value };
1703
+ },
1704
+ async promptManualOperator({ initialValue, allowBack }) {
1705
+ const options = [
1706
+ { label: "lt (less than)", value: "lt" },
1707
+ { label: "lte (less than or equal)", value: "lte" },
1708
+ { label: "eq (equal)", value: "eq" },
1709
+ { label: "gte (greater than or equal)", value: "gte" },
1710
+ { label: "gt (greater than)", value: "gt" }
1711
+ ];
1712
+ if (allowBack) {
1713
+ options.unshift({ label: "Back", value: BACK_VALUE3 });
1714
+ }
1715
+ const value = await (0, import_prompts6.select)({
1716
+ message: "Leading indicator comparison operator",
1717
+ options,
1718
+ initialValue
1719
+ });
1720
+ if ((0, import_prompts6.isCancel)(value)) {
1721
+ return { kind: "cancel" };
1722
+ }
1723
+ if (value === BACK_VALUE3) {
1724
+ return { kind: "back" };
1725
+ }
1726
+ return { kind: "value", value };
1727
+ },
1728
+ async promptManualTarget({ initialValue, allowBack }) {
1729
+ const backHint = allowBack ? ` ${DIM3}(type b to go back)${RESET3}` : "";
1730
+ const value = await (0, import_prompts6.text)({
1731
+ message: `Leading indicator numeric target (required).${backHint}`,
1732
+ initialValue: typeof initialValue === "number" ? String(initialValue) : void 0,
1733
+ validate(rawValue) {
1734
+ const trimmed2 = rawValue.trim();
1735
+ if (allowBack && trimmed2.toLowerCase() === "b") {
1736
+ return;
1737
+ }
1738
+ if (trimmed2.length === 0 || !Number.isFinite(Number(trimmed2))) {
1739
+ return "Enter a valid number.";
1740
+ }
1741
+ }
1742
+ });
1743
+ if ((0, import_prompts6.isCancel)(value)) {
1744
+ return { kind: "cancel" };
1745
+ }
1746
+ const trimmed = value.trim();
1747
+ if (allowBack && trimmed.toLowerCase() === "b") {
1748
+ return { kind: "back" };
1749
+ }
1750
+ return { kind: "value", value: Number(trimmed) };
1751
+ },
1752
+ async promptMixpanelProjectId({ initialValue, allowBack }) {
1753
+ return mixpanelPromptClient.promptMixpanelProjectId({ initialValue, allowBack });
1754
+ },
1755
+ async promptMixpanelWorkspaceId({ initialValue, allowBack }) {
1756
+ return mixpanelPromptClient.promptMixpanelWorkspaceId({ initialValue, allowBack });
1757
+ },
1758
+ async promptMixpanelBookmarkId({ initialValue, allowBack }) {
1759
+ return mixpanelPromptClient.promptMixpanelBookmarkId({ initialValue, allowBack });
1760
+ },
1761
+ async promptMixpanelOperator({ initialValue, allowBack }) {
1762
+ return mixpanelPromptClient.promptMixpanelOperator({ initialValue, allowBack });
1763
+ },
1764
+ async promptMixpanelTarget({ initialValue, allowBack }) {
1765
+ return mixpanelPromptClient.promptMixpanelTarget({ initialValue, allowBack });
1766
+ },
1767
+ async promptPrimaryAssumption({ initialValue, allowBack }) {
1768
+ const backHint = allowBack ? ` ${DIM3}(type b to go back)${RESET3}` : "";
1769
+ const value = await (0, import_prompts6.text)({
1770
+ message: `Primary assumption (required).${backHint}`,
1771
+ initialValue,
1772
+ validate(rawValue) {
1773
+ const trimmed2 = rawValue.trim();
1774
+ if (allowBack && trimmed2.toLowerCase() === "b") {
1775
+ return;
1776
+ }
1777
+ if (trimmed2.length === 0) {
1778
+ return "Enter a value.";
1779
+ }
1780
+ }
1781
+ });
1782
+ if ((0, import_prompts6.isCancel)(value)) {
1783
+ return { kind: "cancel" };
1784
+ }
1785
+ const trimmed = value.trim();
1786
+ if (allowBack && trimmed.toLowerCase() === "b") {
1787
+ return { kind: "back" };
1788
+ }
1789
+ return { kind: "value", value: trimmed };
1790
+ },
1791
+ async promptRationale({ initialValue, allowBack }) {
1792
+ const backHint = allowBack ? ` ${DIM3}(type b to go back)${RESET3}` : "";
1793
+ const value = await (0, import_prompts6.text)({
1794
+ message: `Rationale (required).${backHint}`,
1795
+ initialValue,
1796
+ validate(rawValue) {
1797
+ const trimmed2 = rawValue.trim();
1798
+ if (allowBack && trimmed2.toLowerCase() === "b") {
1799
+ return;
1800
+ }
1801
+ if (trimmed2.length === 0) {
1802
+ return "Enter a value.";
1803
+ }
1804
+ }
1805
+ });
1806
+ if ((0, import_prompts6.isCancel)(value)) {
1807
+ return { kind: "cancel" };
1808
+ }
1809
+ const trimmed = value.trim();
1810
+ if (allowBack && trimmed.toLowerCase() === "b") {
1811
+ return { kind: "back" };
1812
+ }
1813
+ return { kind: "value", value: trimmed };
1814
+ },
1815
+ async promptValidationPlan({ initialValue, allowBack }) {
1816
+ const backHint = allowBack ? ` ${DIM3}(type b to go back)${RESET3}` : "";
1817
+ const value = await (0, import_prompts6.text)({
1818
+ message: `Validation plan (required).${backHint}`,
1819
+ initialValue,
1820
+ validate(rawValue) {
1821
+ const trimmed2 = rawValue.trim();
1822
+ if (allowBack && trimmed2.toLowerCase() === "b") {
1823
+ return;
1824
+ }
1825
+ if (trimmed2.length === 0) {
1826
+ return "Enter a value.";
1827
+ }
1828
+ }
1829
+ });
1830
+ if ((0, import_prompts6.isCancel)(value)) {
1831
+ return { kind: "cancel" };
1832
+ }
1833
+ const trimmed = value.trim();
1834
+ if (allowBack && trimmed.toLowerCase() === "b") {
1835
+ return { kind: "back" };
1836
+ }
1837
+ return { kind: "value", value: trimmed };
1838
+ },
1839
+ async promptNotes({ initialValue, allowBack }) {
1840
+ const backHint = allowBack ? ` ${DIM3}(type b to go back)${RESET3}` : "";
1841
+ const value = await (0, import_prompts6.text)({
1842
+ message: `Notes (optional).${backHint}`,
1843
+ initialValue,
1844
+ validate(rawValue) {
1845
+ const trimmed2 = (rawValue || "").trim();
1846
+ if (allowBack && trimmed2.toLowerCase() === "b") {
1847
+ return;
1848
+ }
1849
+ return void 0;
1850
+ }
1851
+ });
1852
+ if ((0, import_prompts6.isCancel)(value)) {
1853
+ return { kind: "cancel" };
1854
+ }
1855
+ const trimmed = (value || "").trim();
1856
+ if (allowBack && trimmed.toLowerCase() === "b") {
1857
+ return { kind: "back" };
1858
+ }
1859
+ return { kind: "value", value: trimmed };
1860
+ }
1861
+ };
1862
+ }
1863
+
1864
+ // src/ui/newBetName.ts
1865
+ var import_prompts7 = require("@clack/prompts");
1866
+ function normalizeBetName(value) {
1867
+ return value.trim().replace(/\s+/g, "_").toLowerCase();
1868
+ }
1869
+ async function promptNewBetName() {
1870
+ const response = await (0, import_prompts7.text)({
1871
+ message: "Bet name",
1872
+ placeholder: "Landing page iteration",
1873
+ validate(input) {
1874
+ return input.trim().length > 0 ? void 0 : "Bet name is required.";
1875
+ }
1876
+ });
1877
+ if ((0, import_prompts7.isCancel)(response)) {
1878
+ return { cancelled: true };
1879
+ }
1880
+ return {
1881
+ cancelled: false,
1882
+ value: normalizeBetName(response)
1883
+ };
1884
+ }
1885
+
1886
+ // src/commands/new.ts
1887
+ async function pathExists3(filePath) {
1888
+ try {
1889
+ await (0, import_promises7.access)(filePath);
1890
+ return true;
1891
+ } catch {
1892
+ return false;
1893
+ }
1894
+ }
1895
+ function isInteractiveTty3() {
1896
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1897
+ }
1898
+ function invalidIdError(id) {
1899
+ return `Invalid bet id '${id}'. Use id format like 'landing-page' or 'landing_page'.`;
1900
+ }
1901
+ async function runNew(rawId) {
1902
+ let id = rawId ? normalizeBetName(rawId) : void 0;
1903
+ if (!id) {
1904
+ if (!isInteractiveTty3()) {
1905
+ console.error("Missing bet name. Run 'bep new <name>' or use an interactive terminal.");
1906
+ return 1;
1907
+ }
1908
+ const nameResult = await promptNewBetName();
1909
+ if (nameResult.cancelled) {
1910
+ console.error("Cancelled. No files were created.");
1911
+ return 1;
1912
+ }
1913
+ id = normalizeBetName(nameResult.value);
1914
+ }
1915
+ if (!isValidBetId(id)) {
1916
+ console.error(invalidIdError(id));
1917
+ return 1;
1918
+ }
1919
+ let rootDir;
1920
+ try {
1921
+ const cwd = process.cwd();
1922
+ ({ rootDir } = await ensureInitializedRepo(cwd));
1923
+ } catch (error) {
1924
+ console.error(error.message);
1925
+ return 1;
1926
+ }
1927
+ const relativePath = import_node_path8.default.join(BETS_DIR, `${id}.md`);
1928
+ const absolutePath = import_node_path8.default.join(rootDir, relativePath);
1929
+ if (await pathExists3(absolutePath)) {
1930
+ console.error(`Bet '${id}' already exists at ${relativePath}. Choose a unique id.`);
1931
+ return 1;
1932
+ }
1933
+ const wizardResult = await runNewWizard();
1934
+ if (wizardResult.cancelled) {
1935
+ console.error("Cancelled. No files were created.");
1936
+ return 1;
1937
+ }
1938
+ const markdown = renderNewBetMarkdown({
1939
+ id,
1940
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1941
+ leadingIndicator: wizardResult.values.leadingIndicator,
1942
+ maxHours: wizardResult.values.maxHours,
1943
+ maxCalendarDays: wizardResult.values.maxCalendarDays,
1944
+ primaryAssumption: wizardResult.values.primaryAssumption,
1945
+ rationale: wizardResult.values.rationale,
1946
+ validationPlan: wizardResult.values.validationPlan,
1947
+ notes: wizardResult.values.notes
1948
+ });
1949
+ try {
1950
+ await (0, import_promises7.writeFile)(absolutePath, markdown, { encoding: "utf8", flag: "wx" });
1951
+ } catch (error) {
1952
+ if (error.code === "EEXIST") {
1953
+ console.error(`Bet '${id}' already exists at ${relativePath}. Choose a unique id.`);
1954
+ return 1;
1955
+ }
1956
+ throw error;
1957
+ }
1958
+ console.log(`
1959
+ Created ${relativePath}.`);
1960
+ return 0;
1961
+ }
1962
+
1963
+ // src/state/state.ts
1964
+ var import_promises8 = require("fs/promises");
1965
+ var import_node_path9 = __toESM(require("path"));
1966
+ function isValidActiveSession(value) {
1967
+ if (!value || typeof value !== "object") {
1968
+ return false;
1969
+ }
1970
+ const candidate = value;
1971
+ return typeof candidate.id === "string" && candidate.id.length > 0 && typeof candidate.started_at === "string";
1972
+ }
1973
+ function parseState(raw) {
1974
+ const parsed = JSON.parse(raw);
1975
+ if (!parsed || typeof parsed !== "object") {
1976
+ throw new Error("State file must contain a JSON object.");
1977
+ }
1978
+ const active = parsed.active;
1979
+ if (!Array.isArray(active)) {
1980
+ throw new Error("State file field 'active' must be an array.");
1981
+ }
1982
+ for (const [index, session] of active.entries()) {
1983
+ if (!isValidActiveSession(session)) {
1984
+ throw new Error(`State file has invalid active session at index ${index}.`);
1985
+ }
1986
+ }
1987
+ return { active };
1988
+ }
1989
+ async function readState(rootDir) {
1990
+ const statePath = import_node_path9.default.join(rootDir, STATE_PATH);
1991
+ const raw = await (0, import_promises8.readFile)(statePath, "utf8");
1992
+ return parseState(raw);
1993
+ }
1994
+ async function writeState(rootDir, state) {
1995
+ const statePath = import_node_path9.default.join(rootDir, STATE_PATH);
1996
+ await (0, import_promises8.writeFile)(statePath, `${JSON.stringify(state, null, 2)}
1997
+ `, "utf8");
1998
+ }
1999
+ function addActiveSession(state, id, startedAt) {
2000
+ const alreadyActive = state.active.some((session) => session.id === id);
2001
+ if (alreadyActive) {
2002
+ return { state, alreadyActive: true };
2003
+ }
2004
+ return {
2005
+ alreadyActive: false,
2006
+ state: {
2007
+ active: [...state.active, { id, started_at: startedAt }]
2008
+ }
2009
+ };
2010
+ }
2011
+ function removeActiveSessions(state, id) {
2012
+ const removed = state.active.filter((session) => session.id === id);
2013
+ if (removed.length === 0) {
2014
+ return { state, removed };
2015
+ }
2016
+ return {
2017
+ removed,
2018
+ state: {
2019
+ active: state.active.filter((session) => session.id !== id)
2020
+ }
2021
+ };
2022
+ }
2023
+
2024
+ // src/commands/start.ts
2025
+ async function runStart(id) {
2026
+ if (!isValidBetId(id)) {
2027
+ console.error(`Invalid bet id '${id}'. Use lowercase id format like 'landing-page' or 'landing_page'.`);
2028
+ return 1;
2029
+ }
2030
+ let rootDir;
2031
+ try {
2032
+ const cwd = process.cwd();
2033
+ ({ rootDir } = await ensureInitializedRepo(cwd));
2034
+ } catch (error) {
2035
+ console.error(error.message);
2036
+ return 1;
2037
+ }
2038
+ const relativePath = getBetRelativePath(id);
2039
+ const absolutePath = getBetAbsolutePath(rootDir, id);
2040
+ if (!await pathExists2(absolutePath)) {
2041
+ console.error(`Bet '${id}' does not exist at ${relativePath}. Run 'bep new ${id}' first.`);
2042
+ return 1;
2043
+ }
2044
+ try {
2045
+ await readBetFile(rootDir, id);
2046
+ } catch (error) {
2047
+ console.error(error.message);
2048
+ return 1;
2049
+ }
2050
+ let state;
2051
+ try {
2052
+ state = await readState(rootDir);
2053
+ } catch (error) {
2054
+ console.error(`Failed to read state file at bets/_state.json: ${error.message}`);
2055
+ return 1;
2056
+ }
2057
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2058
+ const next = addActiveSession(state, id, now);
2059
+ if (next.alreadyActive) {
2060
+ console.log(`Bet '${id}' is already active.`);
2061
+ return 0;
2062
+ }
2063
+ try {
2064
+ await writeState(rootDir, next.state);
2065
+ } catch (error) {
2066
+ console.error(`Failed to start bet '${id}': ${error.message}`);
2067
+ return 1;
2068
+ }
2069
+ console.log(`Started bet '${id}'.`);
2070
+ return 0;
2071
+ }
2072
+
2073
+ // src/commands/stop.ts
2074
+ var import_promises9 = require("fs/promises");
2075
+ var import_node_path10 = __toESM(require("path"));
2076
+ async function runStop(id) {
2077
+ if (!isValidBetId(id)) {
2078
+ console.error(`Invalid bet id '${id}'. Use lowercase id format like 'landing-page' or 'landing_page'.`);
2079
+ return 1;
2080
+ }
2081
+ let rootDir;
2082
+ try {
2083
+ const cwd = process.cwd();
2084
+ ({ rootDir } = await ensureInitializedRepo(cwd));
2085
+ } catch (error) {
2086
+ console.error(error.message);
2087
+ return 1;
2088
+ }
2089
+ let state;
2090
+ try {
2091
+ state = await readState(rootDir);
2092
+ } catch (error) {
2093
+ console.error(`Failed to read state file at bets/_state.json: ${error.message}`);
2094
+ return 1;
2095
+ }
2096
+ const next = removeActiveSessions(state, id);
2097
+ if (next.removed.length === 0) {
2098
+ console.log(`Bet '${id}' is not active.`);
2099
+ return 0;
2100
+ }
2101
+ const stoppedAt = /* @__PURE__ */ new Date();
2102
+ const stoppedAtIso = stoppedAt.toISOString();
2103
+ const logs = [];
2104
+ for (const session of next.removed) {
2105
+ const startedMs = Date.parse(session.started_at);
2106
+ if (Number.isNaN(startedMs)) {
2107
+ console.error(`Active session for '${id}' has invalid started_at: '${session.started_at}'.`);
2108
+ return 1;
2109
+ }
2110
+ const durationSeconds = Math.max(0, Math.floor((stoppedAt.getTime() - startedMs) / 1e3));
2111
+ logs.push({
2112
+ id,
2113
+ started_at: session.started_at,
2114
+ stopped_at: stoppedAtIso,
2115
+ duration_seconds: durationSeconds
2116
+ });
2117
+ }
2118
+ const relativeBetPath = getBetRelativePath(id);
2119
+ const absoluteBetPath = getBetAbsolutePath(rootDir, id);
2120
+ const hasBetFile = await pathExists2(absoluteBetPath);
2121
+ if (hasBetFile) {
2122
+ try {
2123
+ await readBetFile(rootDir, id);
2124
+ } catch (error) {
2125
+ console.error(error.message);
2126
+ return 1;
2127
+ }
2128
+ }
2129
+ const logPath = import_node_path10.default.join(rootDir, LOGS_DIR, `${id}.jsonl`);
2130
+ const serializedLogs = logs.map((line) => JSON.stringify(line)).join("\n").concat("\n");
2131
+ try {
2132
+ await (0, import_promises9.appendFile)(logPath, serializedLogs, "utf8");
2133
+ await writeState(rootDir, next.state);
2134
+ } catch (error) {
2135
+ console.error(`Failed to stop bet '${id}': ${error.message}`);
2136
+ return 1;
2137
+ }
2138
+ if (!hasBetFile) {
2139
+ console.error(`Warning: Bet file '${relativeBetPath}' is missing. Session was stopped and logged.`);
2140
+ }
2141
+ console.log(`Stopped bet '${id}' (${next.removed.length} session(s) logged).`);
2142
+ return 0;
2143
+ }
2144
+
2145
+ // src/commands/status.ts
2146
+ var import_promises10 = require("fs/promises");
2147
+ var import_node_path11 = __toESM(require("path"));
2148
+ var STATUS_COLUMNS = [
2149
+ "id",
2150
+ "status",
2151
+ "active",
2152
+ "exposureHours",
2153
+ "cap",
2154
+ "capPercent",
2155
+ "warning",
2156
+ "validation"
2157
+ ];
2158
+ var STATUS_HEADERS = {
2159
+ id: "id",
2160
+ status: "status",
2161
+ active: "active",
2162
+ exposureHours: "time_h",
2163
+ cap: "cap",
2164
+ capPercent: "cap_%",
2165
+ warning: "warning",
2166
+ validation: "validation"
2167
+ };
2168
+ function formatHours(value) {
2169
+ return value.toFixed(2);
2170
+ }
2171
+ function formatPercent(value) {
2172
+ return `${value.toFixed(2)}%`;
2173
+ }
2174
+ function parseMaxHours(frontmatter) {
2175
+ const value = frontmatter.max_hours;
2176
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2177
+ return null;
2178
+ }
2179
+ return value;
2180
+ }
2181
+ function parseMaxCalendarDays(frontmatter) {
2182
+ const value = frontmatter.max_calendar_days;
2183
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2184
+ return null;
2185
+ }
2186
+ return value;
2187
+ }
2188
+ function parseCreatedAtMs(frontmatter) {
2189
+ const value = frontmatter.created_at;
2190
+ if (value instanceof Date) {
2191
+ const millis2 = value.getTime();
2192
+ return Number.isNaN(millis2) ? null : millis2;
2193
+ }
2194
+ if (typeof value !== "string" || value.trim().length === 0) {
2195
+ return null;
2196
+ }
2197
+ const millis = Date.parse(value);
2198
+ if (Number.isNaN(millis)) {
2199
+ return null;
2200
+ }
2201
+ return millis;
2202
+ }
2203
+ function formatValidation(snapshot) {
2204
+ const meetsTarget = snapshot.meets_target;
2205
+ const observedValue = snapshot.observed_value;
2206
+ const leadingIndicator = snapshot.leading_indicator;
2207
+ if (typeof meetsTarget !== "boolean" || typeof observedValue !== "number" || !Number.isFinite(observedValue)) {
2208
+ return "N/A";
2209
+ }
2210
+ const resultLabel = meetsTarget ? "PASS" : "FAIL";
2211
+ if (!leadingIndicator || typeof leadingIndicator !== "object") {
2212
+ return `${resultLabel} ${observedValue}`;
2213
+ }
2214
+ const candidate = leadingIndicator;
2215
+ if ((candidate.type === "manual" || candidate.type === "mixpanel") && typeof candidate.target === "number" && Number.isFinite(candidate.target) && (candidate.operator === "lt" || candidate.operator === "lte" || candidate.operator === "eq" || candidate.operator === "gte" || candidate.operator === "gt")) {
2216
+ return `${resultLabel} ${observedValue} ${formatManualComparisonOperator(candidate.operator)} ${candidate.target}`;
2217
+ }
2218
+ return `${resultLabel} ${observedValue}`;
2219
+ }
2220
+ async function sumLoggedExposureSeconds(rootDir, id) {
2221
+ const relativePath = import_node_path11.default.join(LOGS_DIR, `${id}.jsonl`);
2222
+ const absolutePath = import_node_path11.default.join(rootDir, relativePath);
2223
+ if (!await pathExists2(absolutePath)) {
2224
+ return 0;
2225
+ }
2226
+ const raw = await (0, import_promises10.readFile)(absolutePath, "utf8");
2227
+ if (raw.trim().length === 0) {
2228
+ return 0;
2229
+ }
2230
+ let total = 0;
2231
+ const lines = raw.split(/\r?\n/);
2232
+ for (const [index, line] of lines.entries()) {
2233
+ if (line.trim().length === 0) {
2234
+ continue;
2235
+ }
2236
+ let parsed;
2237
+ try {
2238
+ parsed = JSON.parse(line);
2239
+ } catch {
2240
+ throw new Error(`Failed to parse log file at ${relativePath}: invalid JSON on line ${index + 1}.`);
2241
+ }
2242
+ const duration = parsed.duration_seconds;
2243
+ if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) {
2244
+ throw new Error(
2245
+ `Failed to parse log file at ${relativePath}: missing numeric duration_seconds on line ${index + 1}.`
2246
+ );
2247
+ }
2248
+ total += duration;
2249
+ }
2250
+ return total;
2251
+ }
2252
+ async function readValidationLabel(rootDir, id) {
2253
+ const relativePath = import_node_path11.default.join(EVIDENCE_DIR, `${id}.json`);
2254
+ const absolutePath = import_node_path11.default.join(rootDir, relativePath);
2255
+ if (!await pathExists2(absolutePath)) {
2256
+ return "N/A";
2257
+ }
2258
+ let parsed;
2259
+ try {
2260
+ parsed = JSON.parse(await (0, import_promises10.readFile)(absolutePath, "utf8"));
2261
+ } catch (error) {
2262
+ throw new Error(`Failed to parse evidence file at ${relativePath}: ${error.message}`);
2263
+ }
2264
+ if (!parsed || typeof parsed !== "object") {
2265
+ return "N/A";
2266
+ }
2267
+ return formatValidation(parsed);
2268
+ }
2269
+ function renderStatusTable(rows) {
2270
+ const matrix = [
2271
+ STATUS_COLUMNS.map((column) => STATUS_HEADERS[column]),
2272
+ ...rows.map((row) => STATUS_COLUMNS.map((column) => row[column]))
2273
+ ];
2274
+ const widths = STATUS_COLUMNS.map(
2275
+ (_column, columnIndex) => matrix.reduce((max, currentRow) => Math.max(max, currentRow[columnIndex].length), 0)
2276
+ );
2277
+ const rendered = matrix.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" "));
2278
+ return rendered.join("\n");
2279
+ }
2280
+ async function runStatus() {
2281
+ let rootDir;
2282
+ try {
2283
+ const cwd = process.cwd();
2284
+ ({ rootDir } = await ensureInitializedRepo(cwd));
2285
+ } catch (error) {
2286
+ console.error(error.message);
2287
+ return 1;
2288
+ }
2289
+ const betDir = import_node_path11.default.join(rootDir, BETS_DIR);
2290
+ let dirEntries;
2291
+ try {
2292
+ dirEntries = await (0, import_promises10.readdir)(betDir, { withFileTypes: true });
2293
+ } catch (error) {
2294
+ console.error(`Failed to read bets directory at ${BETS_DIR}: ${error.message}`);
2295
+ return 1;
2296
+ }
2297
+ const betFiles = dirEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && !entry.name.startsWith("_")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
2298
+ if (betFiles.length === 0) {
2299
+ console.log("No bets found.");
2300
+ return 0;
2301
+ }
2302
+ let activeBetIds;
2303
+ try {
2304
+ const state = await readState(rootDir);
2305
+ activeBetIds = new Set(state.active.map((session) => session.id));
2306
+ } catch (error) {
2307
+ console.error(`Failed to read state file at bets/_state.json: ${error.message}`);
2308
+ return 1;
2309
+ }
2310
+ const rows = [];
2311
+ const nowMs = Date.now();
2312
+ for (const fileName of betFiles) {
2313
+ const id = fileName.slice(0, -".md".length);
2314
+ let bet;
2315
+ try {
2316
+ bet = (await readBetFile(rootDir, fileName)).bet;
2317
+ } catch (error) {
2318
+ console.error(error.message);
2319
+ return 1;
2320
+ }
2321
+ let exposureSeconds;
2322
+ try {
2323
+ exposureSeconds = await sumLoggedExposureSeconds(rootDir, id);
2324
+ } catch (error) {
2325
+ console.error(error.message);
2326
+ return 1;
2327
+ }
2328
+ let validationLabel;
2329
+ try {
2330
+ validationLabel = await readValidationLabel(rootDir, id);
2331
+ } catch (error) {
2332
+ console.error(error.message);
2333
+ return 1;
2334
+ }
2335
+ const frontmatter = bet.data;
2336
+ const status = normalizeValidationStatus(frontmatter.status);
2337
+ const maxHours = parseMaxHours(frontmatter);
2338
+ const maxCalendarDays = maxHours === null ? parseMaxCalendarDays(frontmatter) : null;
2339
+ const createdAtMs = maxCalendarDays === null ? null : parseCreatedAtMs(frontmatter);
2340
+ const exposureHours = exposureSeconds / 3600;
2341
+ let cap = "-";
2342
+ let capPercent = null;
2343
+ if (maxHours !== null) {
2344
+ cap = `${formatHours(maxHours)}h`;
2345
+ capPercent = exposureHours / maxHours * 100;
2346
+ } else if (maxCalendarDays !== null) {
2347
+ cap = `${maxCalendarDays.toFixed(2)}d`;
2348
+ if (createdAtMs !== null) {
2349
+ const elapsedCalendarDays = Math.max(0, (nowMs - createdAtMs) / (24 * 60 * 60 * 1e3));
2350
+ capPercent = elapsedCalendarDays / maxCalendarDays * 100;
2351
+ }
2352
+ }
2353
+ const warning = capPercent === null ? "-" : capPercent >= 100 ? "AT_CAP" : capPercent >= 70 ? "NEARING_CAP" : "-";
2354
+ rows.push({
2355
+ id,
2356
+ status,
2357
+ active: activeBetIds.has(id) ? "yes" : "no",
2358
+ exposureHours: formatHours(exposureHours),
2359
+ cap,
2360
+ capPercent: capPercent === null ? "-" : formatPercent(capPercent),
2361
+ warning,
2362
+ validation: validationLabel
2363
+ });
2364
+ }
2365
+ console.log(renderStatusTable(rows));
2366
+ return 0;
2367
+ }
2368
+
2369
+ // src/commands/hook.ts
2370
+ var import_promises14 = require("fs/promises");
2371
+ var import_node_path14 = __toESM(require("path"));
2372
+
2373
+ // src/hooks/events.ts
2374
+ var MAX_FIELD_LENGTH = 2e3;
2375
+ function truncate(value) {
2376
+ if (!value) {
2377
+ return void 0;
2378
+ }
2379
+ return value.length > MAX_FIELD_LENGTH ? `${value.slice(0, MAX_FIELD_LENGTH)}...` : value;
2380
+ }
2381
+ function pickString(source, keys) {
2382
+ for (const key of keys) {
2383
+ const value = source[key];
2384
+ if (typeof value === "string" && value.trim().length > 0) {
2385
+ return value;
2386
+ }
2387
+ }
2388
+ return void 0;
2389
+ }
2390
+ function stringifyJson(value) {
2391
+ if (typeof value === "string") {
2392
+ return value;
2393
+ }
2394
+ if (value === void 0) {
2395
+ return void 0;
2396
+ }
2397
+ try {
2398
+ return JSON.stringify(value);
2399
+ } catch {
2400
+ return void 0;
2401
+ }
2402
+ }
2403
+ function parseHookStdin(raw, event) {
2404
+ const trimmed = raw.trim();
2405
+ if (trimmed.length === 0) {
2406
+ return null;
2407
+ }
2408
+ let parsed;
2409
+ try {
2410
+ parsed = JSON.parse(trimmed);
2411
+ } catch {
2412
+ return null;
2413
+ }
2414
+ if (!parsed || typeof parsed !== "object") {
2415
+ return null;
2416
+ }
2417
+ const source = parsed;
2418
+ const prompt = event === "user-prompt-submit" ? pickString(source, ["prompt", "user_prompt", "message"]) ?? stringifyJson(source["prompt"]) : void 0;
2419
+ const toolName = event === "post-tool-use" || event === "post-tool-use-failure" ? pickString(source, ["tool_name", "toolName", "tool"]) ?? pickString(source, ["matcher"]) : void 0;
2420
+ const toolInput = event === "post-tool-use" || event === "post-tool-use-failure" ? stringifyJson(source["tool_input"] ?? source["input"] ?? source["toolInput"]) : void 0;
2421
+ const toolOutput = event === "post-tool-use" || event === "post-tool-use-failure" ? stringifyJson(source["tool_output"] ?? source["output"] ?? source["toolOutput"] ?? source["error"]) : void 0;
2422
+ return {
2423
+ sessionId: pickString(source, ["session_id", "sessionId"]),
2424
+ prompt: truncate(prompt),
2425
+ toolName: truncate(toolName),
2426
+ toolInput: truncate(toolInput),
2427
+ toolOutput: truncate(toolOutput),
2428
+ transcriptPath: truncate(pickString(source, ["transcript_path", "transcriptPath"])),
2429
+ cwd: truncate(pickString(source, ["cwd", "working_directory", "workingDirectory"])),
2430
+ raw: source
2431
+ };
2432
+ }
2433
+ async function readHookStdin() {
2434
+ if (process.stdin.isTTY) {
2435
+ return "";
2436
+ }
2437
+ return new Promise((resolve, reject) => {
2438
+ let data = "";
2439
+ process.stdin.setEncoding("utf8");
2440
+ process.stdin.on("data", (chunk) => {
2441
+ data += chunk;
2442
+ });
2443
+ process.stdin.on("end", () => resolve(data));
2444
+ process.stdin.on("error", (error) => reject(error));
2445
+ });
2446
+ }
2447
+
2448
+ // src/tracking/context.ts
2449
+ var import_promises11 = require("fs/promises");
2450
+ var import_node_path12 = __toESM(require("path"));
2451
+ var MAX_BET_SUMMARY_CHARS = 800;
2452
+ var MAX_HISTORY_ENTRIES = 20;
2453
+ function summarizeContent(content) {
2454
+ const compact = content.replace(/\s+/g, " ").trim();
2455
+ if (compact.length <= MAX_BET_SUMMARY_CHARS) {
2456
+ return compact;
2457
+ }
2458
+ return `${compact.slice(0, MAX_BET_SUMMARY_CHARS)}...`;
2459
+ }
2460
+ function extractSection(content, heading) {
2461
+ const pattern = new RegExp(`##\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "i");
2462
+ const match = content.match(pattern);
2463
+ if (!match || !match[1]) {
2464
+ return void 0;
2465
+ }
2466
+ return summarizeContent(match[1]);
2467
+ }
2468
+ async function readBetCatalog(rootDir) {
2469
+ const files = await listBetMarkdownFiles(rootDir);
2470
+ const result = [];
2471
+ for (const fileName of files) {
2472
+ try {
2473
+ const parsed = await readBetFile(rootDir, fileName);
2474
+ const id = String(parsed.bet.data.id ?? fileName.replace(/\.md$/, ""));
2475
+ const status = normalizeValidationStatus(parsed.bet.data.status);
2476
+ const content = parsed.bet.content || "";
2477
+ result.push({
2478
+ id,
2479
+ status,
2480
+ assumption: extractSection(content, "1\\. Primary Assumption"),
2481
+ rationale: extractSection(content, "2\\. Rationale"),
2482
+ validationPlan: extractSection(content, "3\\. Validation Plan"),
2483
+ notes: extractSection(content, "4\\. Notes"),
2484
+ summary: summarizeContent(content)
2485
+ });
2486
+ } catch {
2487
+ }
2488
+ }
2489
+ return result;
2490
+ }
2491
+ async function readRecentAttribution(rootDir) {
2492
+ const filePath = import_node_path12.default.join(rootDir, LOGS_DIR, "agent-attribution.jsonl");
2493
+ let raw;
2494
+ try {
2495
+ raw = await (0, import_promises11.readFile)(filePath, "utf8");
2496
+ } catch {
2497
+ return [];
2498
+ }
2499
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
2500
+ const recent = lines.slice(-MAX_HISTORY_ENTRIES);
2501
+ const parsed = [];
2502
+ for (const line of recent) {
2503
+ try {
2504
+ const value = JSON.parse(line);
2505
+ parsed.push(value);
2506
+ } catch {
2507
+ }
2508
+ }
2509
+ return parsed;
2510
+ }
2511
+ async function buildBetSelectionContext(rootDir, event, payload) {
2512
+ const [state, bets, recentAttribution] = await Promise.all([
2513
+ readState(rootDir),
2514
+ readBetCatalog(rootDir),
2515
+ readRecentAttribution(rootDir)
2516
+ ]);
2517
+ return {
2518
+ event,
2519
+ payload,
2520
+ activeBetIds: state.active.map((session) => session.id),
2521
+ bets,
2522
+ recentAttribution
2523
+ };
2524
+ }
2525
+
2526
+ // src/tracking/decision.ts
2527
+ var DEFAULT_CONFIDENCE_THRESHOLD = 0.75;
2528
+ var defaultDeps = {
2529
+ start: runStart,
2530
+ stop: runStop
2531
+ };
2532
+ function noOp(decision) {
2533
+ return {
2534
+ applied: false,
2535
+ appliedSteps: [],
2536
+ decision
2537
+ };
2538
+ }
2539
+ function hasValidBet(decision, knownBetIds, key) {
2540
+ const id = decision[key];
2541
+ if (!id) {
2542
+ return false;
2543
+ }
2544
+ return isValidBetId(id) && knownBetIds.has(id);
2545
+ }
2546
+ async function applySelectionDecision(context, rawDecision, deps = defaultDeps) {
2547
+ const confidence = Number.isFinite(rawDecision.confidence) ? rawDecision.confidence : 0;
2548
+ const decision = {
2549
+ ...rawDecision,
2550
+ confidence
2551
+ };
2552
+ const knownBetIds = new Set(context.bets.map((bet) => bet.id));
2553
+ if (decision.confidence < DEFAULT_CONFIDENCE_THRESHOLD) {
2554
+ return noOp({
2555
+ ...decision,
2556
+ action: "none",
2557
+ reason: `${decision.reason} (below confidence threshold)`
2558
+ });
2559
+ }
2560
+ if (decision.action === "none" || decision.action === "keep") {
2561
+ return noOp(decision);
2562
+ }
2563
+ if (decision.action === "start") {
2564
+ if (!hasValidBet(decision, knownBetIds, "bet_id")) {
2565
+ return noOp({ ...decision, action: "none", reason: `${decision.reason} (invalid start bet)` });
2566
+ }
2567
+ const code = await deps.start(decision.bet_id);
2568
+ if (code !== 0) {
2569
+ return {
2570
+ applied: false,
2571
+ appliedSteps: [],
2572
+ decision,
2573
+ error: `Failed to start bet '${decision.bet_id}'.`
2574
+ };
2575
+ }
2576
+ return {
2577
+ applied: true,
2578
+ appliedSteps: [`start:${decision.bet_id}`],
2579
+ decision
2580
+ };
2581
+ }
2582
+ if (decision.action === "stop") {
2583
+ if (!hasValidBet(decision, knownBetIds, "bet_id")) {
2584
+ return noOp({ ...decision, action: "none", reason: `${decision.reason} (invalid stop bet)` });
2585
+ }
2586
+ const code = await deps.stop(decision.bet_id);
2587
+ if (code !== 0) {
2588
+ return {
2589
+ applied: false,
2590
+ appliedSteps: [],
2591
+ decision,
2592
+ error: `Failed to stop bet '${decision.bet_id}'.`
2593
+ };
2594
+ }
2595
+ return {
2596
+ applied: true,
2597
+ appliedSteps: [`stop:${decision.bet_id}`],
2598
+ decision
2599
+ };
2600
+ }
2601
+ if (decision.action === "switch") {
2602
+ const hasStart = hasValidBet(decision, knownBetIds, "bet_id");
2603
+ if (!hasStart) {
2604
+ return noOp({ ...decision, action: "none", reason: `${decision.reason} (invalid switch target)` });
2605
+ }
2606
+ const stopId = hasValidBet(decision, knownBetIds, "stop_bet_id") ? decision.stop_bet_id : context.activeBetIds.find((id) => id !== decision.bet_id);
2607
+ if (stopId && stopId === decision.bet_id) {
2608
+ return noOp({ ...decision, action: "keep", reason: `${decision.reason} (switch target already active)` });
2609
+ }
2610
+ const appliedSteps = [];
2611
+ if (stopId) {
2612
+ const stopCode = await deps.stop(stopId);
2613
+ if (stopCode !== 0) {
2614
+ return {
2615
+ applied: false,
2616
+ appliedSteps,
2617
+ decision,
2618
+ error: `Failed to stop bet '${stopId}' during switch.`
2619
+ };
2620
+ }
2621
+ appliedSteps.push(`stop:${stopId}`);
2622
+ }
2623
+ const startCode = await deps.start(decision.bet_id);
2624
+ if (startCode !== 0) {
2625
+ return {
2626
+ applied: false,
2627
+ appliedSteps,
2628
+ decision,
2629
+ error: `Failed to start bet '${decision.bet_id}' during switch.`
2630
+ };
2631
+ }
2632
+ appliedSteps.push(`start:${decision.bet_id}`);
2633
+ return {
2634
+ applied: true,
2635
+ appliedSteps,
2636
+ decision
2637
+ };
2638
+ }
2639
+ return noOp({ ...decision, action: "none", reason: `${decision.reason} (unsupported action)` });
2640
+ }
2641
+
2642
+ // src/tracking/enforcement.ts
2643
+ var import_promises12 = require("fs/promises");
2644
+ var import_node_path13 = __toESM(require("path"));
2645
+ function parsePositiveNumber(value) {
2646
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2647
+ return null;
2648
+ }
2649
+ return value;
2650
+ }
2651
+ function parseCreatedAtMs2(value) {
2652
+ if (value instanceof Date) {
2653
+ const millis2 = value.getTime();
2654
+ return Number.isNaN(millis2) ? null : millis2;
2655
+ }
2656
+ if (typeof value !== "string" || value.trim().length === 0) {
2657
+ return null;
2658
+ }
2659
+ const millis = Date.parse(value);
2660
+ if (Number.isNaN(millis)) {
2661
+ return null;
2662
+ }
2663
+ return millis;
2664
+ }
2665
+ async function sumLoggedExposureSeconds2(rootDir, betId) {
2666
+ const relativePath = import_node_path13.default.join(LOGS_DIR, `${betId}.jsonl`);
2667
+ const absolutePath = import_node_path13.default.join(rootDir, relativePath);
2668
+ if (!await pathExists2(absolutePath)) {
2669
+ return 0;
2670
+ }
2671
+ const raw = await (0, import_promises12.readFile)(absolutePath, "utf8");
2672
+ if (raw.trim().length === 0) {
2673
+ return 0;
2674
+ }
2675
+ let total = 0;
2676
+ const lines = raw.split(/\r?\n/);
2677
+ for (const [index, line] of lines.entries()) {
2678
+ if (line.trim().length === 0) {
2679
+ continue;
2680
+ }
2681
+ let parsed;
2682
+ try {
2683
+ parsed = JSON.parse(line);
2684
+ } catch {
2685
+ throw new Error(`invalid_json_line_${index + 1}`);
2686
+ }
2687
+ const duration = parsed.duration_seconds;
2688
+ if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) {
2689
+ throw new Error(`invalid_duration_line_${index + 1}`);
2690
+ }
2691
+ total += duration;
2692
+ }
2693
+ return total;
2694
+ }
2695
+ async function calculateExposureForBet(rootDir, betId, createdAt) {
2696
+ const seconds = await sumLoggedExposureSeconds2(rootDir, betId);
2697
+ const hours = seconds / 3600;
2698
+ const createdAtMs = parseCreatedAtMs2(createdAt);
2699
+ if (createdAtMs === null) {
2700
+ return {
2701
+ hours,
2702
+ calendarDays: null
2703
+ };
2704
+ }
2705
+ const calendarDays = Math.max(0, (Date.now() - createdAtMs) / (24 * 60 * 60 * 1e3));
2706
+ return {
2707
+ hours,
2708
+ calendarDays
2709
+ };
2710
+ }
2711
+ function selectGateTargetBet(context, decision) {
2712
+ if (decision.bet_id && isValidBetId(decision.bet_id)) {
2713
+ return decision.bet_id;
2714
+ }
2715
+ if (decision.action === "switch" && decision.bet_id && isValidBetId(decision.bet_id)) {
2716
+ return decision.bet_id;
2717
+ }
2718
+ if (context.activeBetIds.length === 1) {
2719
+ const active = context.activeBetIds[0];
2720
+ return isValidBetId(active) ? active : null;
2721
+ }
2722
+ return null;
2723
+ }
2724
+ async function evaluateCapGate(rootDir, context, decision) {
2725
+ const targetBetId = selectGateTargetBet(context, decision);
2726
+ if (!targetBetId) {
2727
+ return {
2728
+ overCap: false,
2729
+ reason: "no_target_bet"
2730
+ };
2731
+ }
2732
+ const catalogEntry = context.bets.find((bet2) => bet2.id === targetBetId);
2733
+ if (catalogEntry?.status === "passed") {
2734
+ return {
2735
+ targetBetId,
2736
+ overCap: false,
2737
+ reason: "bet_passed"
2738
+ };
2739
+ }
2740
+ let bet;
2741
+ try {
2742
+ bet = (await readBetFile(rootDir, targetBetId)).bet;
2743
+ } catch {
2744
+ return {
2745
+ targetBetId,
2746
+ overCap: false,
2747
+ reason: "target_bet_unreadable"
2748
+ };
2749
+ }
2750
+ const maxHours = parsePositiveNumber(bet.data.max_hours);
2751
+ const maxCalendarDays = maxHours === null ? parsePositiveNumber(bet.data.max_calendar_days) : null;
2752
+ if (maxHours === null && maxCalendarDays === null) {
2753
+ return {
2754
+ targetBetId,
2755
+ overCap: false,
2756
+ reason: "no_cap_configured"
2757
+ };
2758
+ }
2759
+ let exposure;
2760
+ try {
2761
+ exposure = await calculateExposureForBet(rootDir, targetBetId, bet.data.created_at);
2762
+ } catch (error) {
2763
+ return {
2764
+ targetBetId,
2765
+ overCap: false,
2766
+ reason: `cap_eval_failed:${error.message}`
2767
+ };
2768
+ }
2769
+ if (maxHours !== null) {
2770
+ const usedValue2 = exposure.hours;
2771
+ const percentUsed2 = usedValue2 / maxHours * 100;
2772
+ return {
2773
+ targetBetId,
2774
+ capType: "max_hours",
2775
+ capValue: maxHours,
2776
+ usedValue: usedValue2,
2777
+ percentUsed: percentUsed2,
2778
+ overCap: percentUsed2 >= 100,
2779
+ reason: percentUsed2 >= 100 ? "at_or_over_cap" : "under_cap"
2780
+ };
2781
+ }
2782
+ const usedValue = exposure.calendarDays;
2783
+ if (usedValue === null || maxCalendarDays === null) {
2784
+ return {
2785
+ targetBetId,
2786
+ capType: "max_calendar_days",
2787
+ capValue: maxCalendarDays ?? void 0,
2788
+ overCap: false,
2789
+ reason: "calendar_cap_missing_created_at"
2790
+ };
2791
+ }
2792
+ const percentUsed = usedValue / maxCalendarDays * 100;
2793
+ return {
2794
+ targetBetId,
2795
+ capType: "max_calendar_days",
2796
+ capValue: maxCalendarDays,
2797
+ usedValue,
2798
+ percentUsed,
2799
+ overCap: percentUsed >= 100,
2800
+ reason: percentUsed >= 100 ? "at_or_over_cap" : "under_cap"
2801
+ };
2802
+ }
2803
+
2804
+ // src/tracking/selector.ts
2805
+ var import_node_child_process = require("child_process");
2806
+ var import_promises13 = require("fs/promises");
2807
+ var import_node_os = __toESM(require("os"));
2808
+ var DEBUG = true;
2809
+ var SELECTION_TIMEOUT_MS = 6e4;
2810
+ var OUTPUT_FORMAT_INSTRUCTION = "Return only one JSON object. Do not include markdown fences, commentary, or extra keys.";
2811
+ function sanitizeForJson(value) {
2812
+ if (value instanceof Error) {
2813
+ return { name: value.name, message: value.message, stack: value.stack };
2814
+ }
2815
+ if (typeof value === "bigint") {
2816
+ return String(value);
2817
+ }
2818
+ return value;
2819
+ }
2820
+ async function writeDebug(debugLogPath, entry) {
2821
+ if (!DEBUG || !debugLogPath) {
2822
+ return;
2823
+ }
2824
+ try {
2825
+ await (0, import_promises13.appendFile)(
2826
+ debugLogPath,
2827
+ `${JSON.stringify({
2828
+ ...entry,
2829
+ data: entry.data ? Object.fromEntries(Object.entries(entry.data).map(([key, value]) => [key, sanitizeForJson(value)])) : void 0
2830
+ })}
2831
+ `,
2832
+ "utf8"
2833
+ );
2834
+ } catch {
2835
+ }
2836
+ }
2837
+ function stripGitEnv(env) {
2838
+ const next = {};
2839
+ for (const [key, value] of Object.entries(env)) {
2840
+ if (key.startsWith("GIT_")) {
2841
+ continue;
2842
+ }
2843
+ next[key] = value;
2844
+ }
2845
+ return next;
2846
+ }
2847
+ async function defaultRunClaude({ prompt, timeoutMs, onDebug }) {
2848
+ return new Promise((resolve, reject) => {
2849
+ void onDebug?.("spawn_start", "Starting claude selector subprocess.", {
2850
+ command: "claude",
2851
+ args: ["--print", "--output-format", "json", "--model", "sonnet", "--setting-sources", ""],
2852
+ cwd: import_node_os.default.tmpdir(),
2853
+ timeoutMs
2854
+ });
2855
+ const child = (0, import_node_child_process.spawn)(
2856
+ "claude",
2857
+ ["--print", "--output-format", "json", "--model", "sonnet", "--setting-sources", ""],
2858
+ {
2859
+ cwd: import_node_os.default.tmpdir(),
2860
+ env: stripGitEnv(process.env),
2861
+ stdio: ["pipe", "pipe", "pipe"]
2862
+ }
2863
+ );
2864
+ let stdout = "";
2865
+ let stderr = "";
2866
+ let timedOut = false;
2867
+ const timer = setTimeout(() => {
2868
+ timedOut = true;
2869
+ child.kill("SIGTERM");
2870
+ }, timeoutMs);
2871
+ child.stdout.setEncoding("utf8");
2872
+ child.stdout.on("data", (chunk) => {
2873
+ stdout += chunk;
2874
+ void onDebug?.("spawn_stdout_chunk", "Received stdout chunk from claude selector.", { chunk });
2875
+ });
2876
+ child.stderr.setEncoding("utf8");
2877
+ child.stderr.on("data", (chunk) => {
2878
+ stderr += chunk;
2879
+ void onDebug?.("spawn_stderr_chunk", "Received stderr chunk from claude selector.", { chunk });
2880
+ });
2881
+ child.on("error", (error) => {
2882
+ clearTimeout(timer);
2883
+ reject(error);
2884
+ });
2885
+ child.on("close", (code) => {
2886
+ clearTimeout(timer);
2887
+ void onDebug?.("spawn_close", "Claude selector subprocess closed.", { code, timedOut, stderr });
2888
+ if (timedOut) {
2889
+ reject(new Error(`Claude selector timed out after ${timeoutMs}ms.`));
2890
+ return;
2891
+ }
2892
+ if (code !== 0) {
2893
+ reject(new Error(`Claude selector failed with exit code ${code}: ${stderr.trim()}`));
2894
+ return;
2895
+ }
2896
+ resolve(stdout);
2897
+ });
2898
+ child.stdin.end(prompt);
2899
+ });
2900
+ }
2901
+ function maybeExtractJsonFromMarkdown(value) {
2902
+ const trimmed = value.trim();
2903
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
2904
+ if (match && match[1]) {
2905
+ return match[1].trim();
2906
+ }
2907
+ return trimmed;
2908
+ }
2909
+ function extractDecisionFromArrayEnvelope(events) {
2910
+ const typed = events.filter((entry) => entry && typeof entry === "object");
2911
+ const resultEvent = typed.find((entry) => entry.type === "result" && typeof entry.result === "string");
2912
+ if (resultEvent && typeof resultEvent.result === "string") {
2913
+ const body = maybeExtractJsonFromMarkdown(resultEvent.result);
2914
+ return JSON.parse(body);
2915
+ }
2916
+ const assistantEvent = typed.find((entry) => entry.type === "assistant" && Array.isArray(entry.message?.content));
2917
+ if (!assistantEvent || !Array.isArray(assistantEvent.message?.content)) {
2918
+ return null;
2919
+ }
2920
+ for (const block of assistantEvent.message.content) {
2921
+ if (block?.type !== "text" || typeof block.text !== "string") {
2922
+ continue;
2923
+ }
2924
+ const body = maybeExtractJsonFromMarkdown(block.text);
2925
+ return JSON.parse(body);
2926
+ }
2927
+ return null;
2928
+ }
2929
+ function toDecision(raw, knownIds) {
2930
+ if (!raw || typeof raw !== "object") {
2931
+ return null;
2932
+ }
2933
+ const candidate = raw;
2934
+ const action = candidate.action;
2935
+ const confidence = candidate.confidence;
2936
+ const reason = candidate.reason;
2937
+ if (action !== "start" && action !== "stop" && action !== "switch" && action !== "keep" && action !== "none") {
2938
+ return null;
2939
+ }
2940
+ if (typeof confidence !== "number" || !Number.isFinite(confidence)) {
2941
+ return null;
2942
+ }
2943
+ if (typeof reason !== "string" || reason.trim().length === 0) {
2944
+ return null;
2945
+ }
2946
+ const betId = typeof candidate.bet_id === "string" ? candidate.bet_id : void 0;
2947
+ const stopBetId = typeof candidate.stop_bet_id === "string" ? candidate.stop_bet_id : void 0;
2948
+ if (betId && (!isValidBetId(betId) || !knownIds.has(betId))) {
2949
+ return null;
2950
+ }
2951
+ if (stopBetId && (!isValidBetId(stopBetId) || !knownIds.has(stopBetId))) {
2952
+ return null;
2953
+ }
2954
+ return {
2955
+ action,
2956
+ bet_id: betId,
2957
+ stop_bet_id: stopBetId,
2958
+ confidence,
2959
+ reason: reason.trim()
2960
+ };
2961
+ }
2962
+ function buildPrompt(context) {
2963
+ const bets = context.bets.map((bet) => ({
2964
+ id: bet.id,
2965
+ status: bet.status,
2966
+ assumption: bet.assumption,
2967
+ rationale: bet.rationale,
2968
+ validation_plan: bet.validationPlan,
2969
+ notes: bet.notes,
2970
+ summary: bet.summary
2971
+ })).slice(0, 50);
2972
+ const payload = context.payload ? {
2973
+ session_id: context.payload.sessionId,
2974
+ prompt: context.payload.prompt,
2975
+ tool_name: context.payload.toolName,
2976
+ tool_input: context.payload.toolInput,
2977
+ tool_output: context.payload.toolOutput,
2978
+ transcript_path: context.payload.transcriptPath,
2979
+ cwd: context.payload.cwd
2980
+ } : null;
2981
+ const recent = context.recentAttribution.map((entry) => ({
2982
+ at: entry.at,
2983
+ event: entry.event,
2984
+ session_id: entry.session_id,
2985
+ decision: entry.decision
2986
+ }));
2987
+ const eventSpecificPolicy = context.event === "session-end" ? [
2988
+ "Event-specific policy for session-end:",
2989
+ "- If an active bet exists, prefer action 'stop' for the current/inferred active bet.",
2990
+ "- Action 'keep' is generally incorrect at session end unless there is a strong explicit reason.",
2991
+ "- Use action 'none' only when no active/inferable bet can be identified."
2992
+ ] : context.event === "user-prompt-submit" ? [
2993
+ "Event-specific policy for user-prompt-submit:",
2994
+ "- Use 'start' or 'switch' only when intent is explicit or strongly inferred.",
2995
+ "- Prefer 'none' when intent is weak or ambiguous."
2996
+ ] : [
2997
+ "Event-specific policy for tool events:",
2998
+ "- Treat tool events as reinforcement signals, not sole evidence for high-confidence switches.",
2999
+ "- Prefer 'none' when evidence is insufficient."
3000
+ ];
3001
+ return [
3002
+ "You are selecting BEP bet attribution actions for a coding session.",
3003
+ "Constraints:",
3004
+ "- Choose only bet IDs listed in provided bets.",
3005
+ "- Prefer action 'none' when uncertain.",
3006
+ "- Never invent bet IDs.",
3007
+ "- Output must follow the required JSON schema.",
3008
+ OUTPUT_FORMAT_INSTRUCTION,
3009
+ "",
3010
+ "Action semantics:",
3011
+ "- start: begin tracking a specific bet.",
3012
+ "- stop: pause tracking a specific active bet.",
3013
+ "- switch: stop old bet and start new bet when shift is clear.",
3014
+ "- keep: tracking should remain unchanged (discouraged for session-end).",
3015
+ "- none: insufficient evidence or no valid target.",
3016
+ "",
3017
+ ...eventSpecificPolicy,
3018
+ "",
3019
+ "Required JSON schema:",
3020
+ '{"action":"start|stop|switch|keep|none","bet_id":"optional","stop_bet_id":"optional","confidence":0-1,"reason":"short"}',
3021
+ "",
3022
+ "Context JSON:",
3023
+ JSON.stringify(
3024
+ {
3025
+ event: context.event,
3026
+ active_bets: context.activeBetIds,
3027
+ payload,
3028
+ bets,
3029
+ recent_attribution: recent
3030
+ },
3031
+ null,
3032
+ 2
3033
+ )
3034
+ ].join("\n");
3035
+ }
3036
+ async function selectBetWithClaude(context, runClaudeOrOptions = defaultRunClaude, maybeOptions = {}) {
3037
+ const runClaude = typeof runClaudeOrOptions === "function" ? runClaudeOrOptions : defaultRunClaude;
3038
+ const options = typeof runClaudeOrOptions === "function" ? maybeOptions : runClaudeOrOptions;
3039
+ const debugLog = (stage, message, data) => writeDebug(options.debugLogPath, {
3040
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3041
+ stage,
3042
+ event: context.event,
3043
+ session_id: context.payload?.sessionId,
3044
+ message,
3045
+ data
3046
+ });
3047
+ if (context.bets.length === 0) {
3048
+ await debugLog("return_ok", "No bets available. Returning no-op decision.");
3049
+ return {
3050
+ ok: true,
3051
+ decision: {
3052
+ action: "none",
3053
+ confidence: 1,
3054
+ reason: "No bets available for attribution."
3055
+ },
3056
+ rawText: ""
3057
+ };
3058
+ }
3059
+ const prompt = buildPrompt(context);
3060
+ await debugLog("build_prompt", "Built Claude selector prompt.", {
3061
+ prompt,
3062
+ activeBetIds: context.activeBetIds,
3063
+ betIds: context.bets.map((bet) => bet.id)
3064
+ });
3065
+ let rawText = "";
3066
+ try {
3067
+ rawText = await runClaude({
3068
+ prompt,
3069
+ timeoutMs: SELECTION_TIMEOUT_MS,
3070
+ onDebug: (stage, message, data) => {
3071
+ void debugLog(stage, message, data);
3072
+ }
3073
+ });
3074
+ } catch (error) {
3075
+ await debugLog("return_error", "Claude selector subprocess failed.", {
3076
+ error
3077
+ });
3078
+ return {
3079
+ ok: false,
3080
+ error: `Failed to run Claude selector: ${error.message}`
3081
+ };
3082
+ }
3083
+ const trimmed = rawText.trim();
3084
+ if (trimmed.length === 0) {
3085
+ await debugLog("return_error", "Claude selector returned empty output.");
3086
+ return {
3087
+ ok: false,
3088
+ error: "Claude selector returned empty output.",
3089
+ rawText
3090
+ };
3091
+ }
3092
+ let outer;
3093
+ try {
3094
+ await debugLog("parse_outer_json", "Parsing outer Claude output JSON.", { rawText });
3095
+ outer = JSON.parse(trimmed);
3096
+ } catch {
3097
+ await debugLog("return_error", "Failed to parse outer Claude output as JSON.", { rawText });
3098
+ return {
3099
+ ok: false,
3100
+ error: "Claude selector returned non-JSON output.",
3101
+ rawText
3102
+ };
3103
+ }
3104
+ let decisionValue = outer;
3105
+ if (Array.isArray(outer)) {
3106
+ try {
3107
+ await debugLog("parse_result_json", "Parsing Claude array envelope.", { eventCount: outer.length });
3108
+ decisionValue = extractDecisionFromArrayEnvelope(outer);
3109
+ } catch {
3110
+ await debugLog("return_error", "Failed to parse decision from Claude array envelope.", { rawText });
3111
+ return {
3112
+ ok: false,
3113
+ error: "Claude selector array envelope did not contain valid JSON decision.",
3114
+ rawText
3115
+ };
3116
+ }
3117
+ if (!decisionValue) {
3118
+ await debugLog("return_error", "Claude array envelope missing decision payload.", { rawText });
3119
+ return {
3120
+ ok: false,
3121
+ error: "Claude selector array envelope did not include a decision payload.",
3122
+ rawText
3123
+ };
3124
+ }
3125
+ } else if (outer && typeof outer === "object" && typeof outer.result === "string") {
3126
+ const body = maybeExtractJsonFromMarkdown(outer.result ?? "");
3127
+ try {
3128
+ await debugLog("parse_result_json", "Parsing Claude envelope result JSON.", { body });
3129
+ decisionValue = JSON.parse(body);
3130
+ } catch {
3131
+ await debugLog("return_error", "Failed to parse Claude envelope result JSON.", { body, rawText });
3132
+ return {
3133
+ ok: false,
3134
+ error: "Claude selector result field did not contain valid JSON decision.",
3135
+ rawText
3136
+ };
3137
+ }
3138
+ }
3139
+ const knownIds = new Set(context.bets.map((bet) => bet.id));
3140
+ const parsedDecision = toDecision(decisionValue, knownIds);
3141
+ await debugLog("validate_decision", "Validating selector decision schema.", {
3142
+ decisionValue,
3143
+ knownIds: Array.from(knownIds),
3144
+ valid: parsedDecision !== null
3145
+ });
3146
+ if (!parsedDecision) {
3147
+ await debugLog("return_error", "Selector decision failed schema validation.", { decisionValue, rawText });
3148
+ return {
3149
+ ok: false,
3150
+ error: "Claude selector decision failed schema validation.",
3151
+ rawText
3152
+ };
3153
+ }
3154
+ if (context.event === "session-end" && parsedDecision.action === "keep") {
3155
+ await debugLog("validate_decision", "Session-end returned keep; this conflicts with preferred stop policy.", {
3156
+ decision: parsedDecision,
3157
+ activeBetIds: context.activeBetIds
3158
+ });
3159
+ }
3160
+ await debugLog("return_ok", "Selector decision parsed successfully.", {
3161
+ decision: parsedDecision
3162
+ });
3163
+ return {
3164
+ ok: true,
3165
+ decision: parsedDecision,
3166
+ rawText
3167
+ };
3168
+ }
3169
+
3170
+ // src/commands/hook.ts
3171
+ var defaultDeps2 = {
3172
+ readInput: readHookStdin,
3173
+ select: selectBetWithClaude,
3174
+ apply: applySelectionDecision,
3175
+ append: import_promises14.appendFile,
3176
+ writeOutput: (output) => process.stdout.write(output)
3177
+ };
3178
+ function isHookEvent(value) {
3179
+ return value === "user-prompt-submit" || value === "post-tool-use" || value === "post-tool-use-failure" || value === "session-end";
3180
+ }
3181
+ function selectLogDecision(selection, applied) {
3182
+ if (applied) {
3183
+ return applied.decision;
3184
+ }
3185
+ if (selection.ok) {
3186
+ return selection.decision;
3187
+ }
3188
+ return {
3189
+ action: "none",
3190
+ confidence: 0,
3191
+ reason: selection.error
3192
+ };
3193
+ }
3194
+ async function runHook(agent, event, deps = defaultDeps2) {
3195
+ if (!isSupportedHookAgent(agent)) {
3196
+ console.error(`Unsupported hook agent '${agent}'. Only 'claude-code' is supported.`);
3197
+ return 1;
3198
+ }
3199
+ if (!isHookEvent(event)) {
3200
+ console.error(
3201
+ `Unsupported hook event '${event}'. Use one of: user-prompt-submit, post-tool-use, post-tool-use-failure, session-end.`
3202
+ );
3203
+ return 1;
3204
+ }
3205
+ const found = await findInitializedRepo(process.cwd());
3206
+ if (!found) {
3207
+ return 0;
3208
+ }
3209
+ const at = (/* @__PURE__ */ new Date()).toISOString();
3210
+ const writeOutput = deps.writeOutput ?? defaultDeps2.writeOutput;
3211
+ const rawInput = await deps.readInput().catch(() => "");
3212
+ const payload = parseHookStdin(rawInput, event);
3213
+ let selection;
3214
+ let applied = null;
3215
+ let error = null;
3216
+ let promptDenied = false;
3217
+ let promptDenyReason = null;
3218
+ let blockLine = null;
3219
+ try {
3220
+ const context = await buildBetSelectionContext(found.rootDir, event, payload);
3221
+ const debugLogPath = import_node_path14.default.join(found.rootDir, LOGS_DIR, "hook_debug.log");
3222
+ selection = await deps.select(context, { debugLogPath });
3223
+ if (selection.ok) {
3224
+ const gate = await evaluateCapGate(found.rootDir, context, selection.decision);
3225
+ if (gate.overCap) {
3226
+ blockLine = {
3227
+ at,
3228
+ agent,
3229
+ event,
3230
+ session_id: payload?.sessionId,
3231
+ bet_id: gate.targetBetId,
3232
+ cap_type: gate.capType,
3233
+ cap_value: gate.capValue,
3234
+ used_value: gate.usedValue,
3235
+ percent_used: gate.percentUsed,
3236
+ over_cap: true,
3237
+ enforced: event === "user-prompt-submit",
3238
+ reason: gate.reason
3239
+ };
3240
+ }
3241
+ if (event === "user-prompt-submit" && gate.overCap) {
3242
+ promptDenied = true;
3243
+ promptDenyReason = `Bet '${gate.targetBetId ?? "unknown"}' is at cap (${(gate.usedValue ?? 0).toFixed(2)} ${gate.capType === "max_calendar_days" ? "days" : "hours"} / ${(gate.capValue ?? 0).toFixed(2)} ${gate.capType === "max_calendar_days" ? "days" : "hours"}, ${(gate.percentUsed ?? 0).toFixed(2)}%). Update bets/${gate.targetBetId}.md to extend cap or change status before continuing.`;
3244
+ } else {
3245
+ applied = await deps.apply(context, selection.decision);
3246
+ if (applied.error) {
3247
+ error = applied.error;
3248
+ }
3249
+ }
3250
+ } else {
3251
+ error = selection.error;
3252
+ }
3253
+ } catch (caught) {
3254
+ const message = caught.message;
3255
+ selection = {
3256
+ ok: false,
3257
+ error: `Hook attribution failed: ${message}`
3258
+ };
3259
+ error = selection.error;
3260
+ }
3261
+ const decision = selectLogDecision(selection, applied);
3262
+ const attributionLine = {
3263
+ at,
3264
+ agent,
3265
+ event,
3266
+ session_id: payload?.sessionId,
3267
+ decision,
3268
+ applied: applied?.applied ?? false,
3269
+ applied_steps: applied?.appliedSteps ?? [],
3270
+ error
3271
+ };
3272
+ const sessionLine = {
3273
+ agent,
3274
+ event,
3275
+ at,
3276
+ session_id: payload?.sessionId,
3277
+ bet_id: decision.bet_id,
3278
+ confidence: decision.confidence,
3279
+ applied: applied?.applied ?? false
3280
+ };
3281
+ const attributionPath = import_node_path14.default.join(found.rootDir, LOGS_DIR, "agent-attribution.jsonl");
3282
+ const sessionPath = import_node_path14.default.join(found.rootDir, LOGS_DIR, "agent-sessions.jsonl");
3283
+ const blocksPath = import_node_path14.default.join(found.rootDir, LOGS_DIR, "agent-blocks.jsonl");
3284
+ if (blockLine) {
3285
+ await deps.append(blocksPath, `${JSON.stringify(blockLine)}
3286
+ `, "utf8");
3287
+ }
3288
+ await deps.append(attributionPath, `${JSON.stringify(attributionLine)}
3289
+ `, "utf8");
3290
+ await deps.append(sessionPath, `${JSON.stringify(sessionLine)}
3291
+ `, "utf8");
3292
+ if (event === "user-prompt-submit" && promptDenied) {
3293
+ writeOutput?.(JSON.stringify({ continue: false, stopReason: promptDenyReason ?? "Bet is hard-blocked at cap." }));
3294
+ return 0;
3295
+ }
3296
+ if (event === "user-prompt-submit") {
3297
+ writeOutput?.(JSON.stringify({ continue: true }));
3298
+ return 0;
3299
+ }
3300
+ writeOutput?.(JSON.stringify({ continue: true }));
3301
+ return 0;
3302
+ }
3303
+
3304
+ // src/cli.ts
3305
+ async function main(argv) {
3306
+ const program = new import_commander.Command();
3307
+ program.name("bep").description("Budgeted Engineering Proposals CLI");
3308
+ program.command("init").description("Initialize BEP directories in the current repository").option("--install-hooks", "Install agent tracking hooks").option("--no-install-hooks", "Skip agent tracking hook setup").option("--agent <agent>", "Agent target for hook setup (currently: claude-code)").action(async (options) => {
3309
+ const exitCode = await runInit({
3310
+ installHooks: options.installHooks,
3311
+ agent: options.agent
3312
+ });
3313
+ process.exitCode = exitCode;
3314
+ });
3315
+ program.command("new [id...]").description("Create a new BEP markdown file").action(async (idParts) => {
3316
+ const id = idParts && idParts.length > 0 ? ["new", ...idParts].join(" ") : void 0;
3317
+ const exitCode = await runNew(id);
3318
+ process.exitCode = exitCode;
3319
+ });
3320
+ program.command("start <id>").description("Start work on an existing BEP").action(async (id) => {
3321
+ const exitCode = await runStart(id);
3322
+ process.exitCode = exitCode;
3323
+ });
3324
+ program.command("stop <id>").description("Stop work on an active BEP and log session exposure").action(async (id) => {
3325
+ const exitCode = await runStop(id);
3326
+ process.exitCode = exitCode;
3327
+ });
3328
+ program.command("status").description("Show status for current bets").action(async () => {
3329
+ const exitCode = await runStatus();
3330
+ process.exitCode = exitCode;
3331
+ });
3332
+ program.command("check <id>").description("Capture validation evidence for a BEP").option("-f, --force", "Re-run provider check even if bet status is passed").action(async (id, options) => {
3333
+ const exitCode = await runCheck(id, { force: options.force });
3334
+ process.exitCode = exitCode;
3335
+ });
3336
+ program.command("hook <agent> <event>").description("Internal command used by agent hook integrations").action(async (agent, event) => {
3337
+ const exitCode = await runHook(agent, event);
3338
+ process.exitCode = exitCode;
3339
+ });
3340
+ await program.parseAsync(argv);
3341
+ }
3342
+ if (require.main === module) {
3343
+ void main(process.argv);
3344
+ }
3345
+ // Annotate the CommonJS export names for ESM import in node:
3346
+ 0 && (module.exports = {
3347
+ main
3348
+ });
3349
+ //# sourceMappingURL=cli.js.map