@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/LICENSE.md +7 -0
- package/README.md +197 -0
- package/dist/cli.js +3349 -0
- package/dist/cli.js.map +1 -0
- package/package.json +37 -0
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
|