jobarbiter 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,690 @@
1
+ /**
2
+ * JobArbiter Observer — Hook installer for coding agent CLIs
3
+ *
4
+ * Detects installed coding agents, installs observation hooks that
5
+ * extract proficiency signals from session transcripts.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ import { execSync } from "node:child_process";
11
+ // ── Agent Detection ────────────────────────────────────────────────────
12
+ const AGENT_DEFINITIONS = [
13
+ {
14
+ id: "claude-code",
15
+ name: "Claude Code",
16
+ configDir: join(homedir(), ".claude"),
17
+ hookFormat: "claude",
18
+ detectBin: "claude",
19
+ },
20
+ {
21
+ id: "cursor",
22
+ name: "Cursor",
23
+ configDir: join(homedir(), ".cursor"),
24
+ hookFormat: "cursor",
25
+ detectBin: null, // Cursor is an app, not a CLI
26
+ detectDir: join(homedir(), ".cursor"),
27
+ },
28
+ {
29
+ id: "opencode",
30
+ name: "OpenCode",
31
+ configDir: join(homedir(), ".config", "opencode"),
32
+ hookFormat: "opencode",
33
+ detectBin: "opencode",
34
+ },
35
+ {
36
+ id: "codex",
37
+ name: "Codex CLI",
38
+ configDir: join(homedir(), ".codex"),
39
+ hookFormat: "codex",
40
+ detectBin: "codex",
41
+ },
42
+ {
43
+ id: "gemini",
44
+ name: "Gemini CLI",
45
+ configDir: join(homedir(), ".gemini"),
46
+ hookFormat: "gemini",
47
+ detectBin: "gemini",
48
+ },
49
+ ];
50
+ function binExists(name) {
51
+ try {
52
+ execSync(`which ${name}`, { stdio: "ignore" });
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ export function detectAgents() {
60
+ return AGENT_DEFINITIONS.map((def) => {
61
+ const installed = (def.detectBin && binExists(def.detectBin)) ||
62
+ existsSync(def.configDir);
63
+ return {
64
+ id: def.id,
65
+ name: def.name,
66
+ configDir: def.configDir,
67
+ hookFormat: def.hookFormat,
68
+ installed: !!installed,
69
+ hookInstalled: installed ? isHookInstalled(def.id, def.configDir, def.hookFormat) : false,
70
+ };
71
+ });
72
+ }
73
+ // ── Hook Detection ─────────────────────────────────────────────────────
74
+ function isHookInstalled(agentId, configDir, format) {
75
+ try {
76
+ switch (format) {
77
+ case "claude":
78
+ case "cursor": {
79
+ const hookFile = join(configDir, "hooks.json");
80
+ if (!existsSync(hookFile))
81
+ return false;
82
+ const content = readFileSync(hookFile, "utf-8");
83
+ return content.includes("jobarbiter");
84
+ }
85
+ case "opencode": {
86
+ const pluginDir = join(configDir, "plugins");
87
+ return existsSync(join(pluginDir, "jobarbiter-observer.ts"));
88
+ }
89
+ case "codex": {
90
+ const configFile = join(configDir, "config.toml");
91
+ if (!existsSync(configFile))
92
+ return false;
93
+ const content = readFileSync(configFile, "utf-8");
94
+ return content.includes("jobarbiter");
95
+ }
96
+ case "gemini": {
97
+ const settingsFile = join(configDir, "settings.json");
98
+ if (!existsSync(settingsFile))
99
+ return false;
100
+ const content = readFileSync(settingsFile, "utf-8");
101
+ return content.includes("jobarbiter");
102
+ }
103
+ default:
104
+ return false;
105
+ }
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ // ── Observer Data Directory ────────────────────────────────────────────
112
+ const OBSERVER_DIR = join(homedir(), ".config", "jobarbiter", "observer");
113
+ const OBSERVATIONS_FILE = join(OBSERVER_DIR, "observations.json");
114
+ const HOOKS_DIR = join(OBSERVER_DIR, "hooks");
115
+ function ensureObserverDirs() {
116
+ mkdirSync(OBSERVER_DIR, { recursive: true });
117
+ mkdirSync(HOOKS_DIR, { recursive: true });
118
+ // Initialize observations file if missing
119
+ if (!existsSync(OBSERVATIONS_FILE)) {
120
+ writeFileSync(OBSERVATIONS_FILE, JSON.stringify({
121
+ version: 1,
122
+ installedAt: new Date().toISOString(),
123
+ agents: {},
124
+ sessions: [],
125
+ accumulated: {
126
+ totalSessions: 0,
127
+ totalTokens: 0,
128
+ toolCounts: {},
129
+ domainSignals: [],
130
+ lastSubmitted: null,
131
+ },
132
+ }, null, 2) + "\n");
133
+ }
134
+ }
135
+ // ── Core Observer Script ───────────────────────────────────────────────
136
+ /**
137
+ * The universal observer script. Runs as a hook in any coding agent.
138
+ * Reads session transcript data from stdin (JSON), extracts proficiency
139
+ * signals, and appends them to the local observations file.
140
+ *
141
+ * This is written as a standalone shell script so it has zero dependencies
142
+ * and works regardless of the user's Node.js setup.
143
+ */
144
+ function getObserverScript() {
145
+ return `#!/usr/bin/env node
146
+ /**
147
+ * JobArbiter Observer Hook
148
+ * Extracts proficiency signals from coding agent sessions.
149
+ *
150
+ * Reads JSON from stdin, writes observations to:
151
+ * ~/.config/jobarbiter/observer/observations.json
152
+ *
153
+ * Signals extracted:
154
+ * - Tool names and frequencies (tool fluency)
155
+ * - Session duration and message counts (output velocity)
156
+ * - File types worked on (domain application)
157
+ * - Token usage when available (token throughput)
158
+ */
159
+
160
+ const fs = require("fs");
161
+ const path = require("path");
162
+ const os = require("os");
163
+
164
+ const OBSERVATIONS_FILE = path.join(
165
+ os.homedir(), ".config", "jobarbiter", "observer", "observations.json"
166
+ );
167
+
168
+ // Read stdin
169
+ let input = "";
170
+ process.stdin.setEncoding("utf-8");
171
+ process.stdin.on("data", (chunk) => { input += chunk; });
172
+ process.stdin.on("end", () => {
173
+ try {
174
+ const data = JSON.parse(input);
175
+ const observation = extractSignals(data);
176
+ if (observation) appendObservation(observation);
177
+ } catch (err) {
178
+ // Silent failure — never block the coding agent
179
+ fs.appendFileSync(
180
+ path.join(os.homedir(), ".config", "jobarbiter", "observer", "errors.log"),
181
+ \`[\${new Date().toISOString()}] \${err.message}\\n\`
182
+ );
183
+ }
184
+ process.exit(0);
185
+ });
186
+
187
+ function extractSignals(data) {
188
+ const observation = {
189
+ timestamp: new Date().toISOString(),
190
+ agent: process.env.JOBARBITER_AGENT || detectAgent(),
191
+ sessionId: data.sessionId || data.session_id || data.thread_id || data.id || null,
192
+ signals: {
193
+ toolsUsed: [],
194
+ fileExtensions: [],
195
+ messageCount: 0,
196
+ thinkingBlocks: 0,
197
+ tokenUsage: null,
198
+ duration: null,
199
+ },
200
+ };
201
+
202
+ // Claude Code / Cursor: stop hook provides session info
203
+ if (data.transcript || data.messages) {
204
+ const messages = data.transcript || data.messages || [];
205
+ observation.signals.messageCount = messages.length;
206
+
207
+ for (const msg of messages) {
208
+ // Extract tool calls
209
+ if (msg.tool_name || msg.toolName) {
210
+ observation.signals.toolsUsed.push(msg.tool_name || msg.toolName);
211
+ }
212
+ if (msg.tool_calls) {
213
+ for (const tc of msg.tool_calls) {
214
+ observation.signals.toolsUsed.push(tc.name || tc.function?.name);
215
+ }
216
+ }
217
+
218
+ // Extract file extensions from tool args
219
+ const args = msg.tool_input || msg.args || msg.tool_calls?.[0]?.input || {};
220
+ const filePath = args.file_path || args.filePath || args.path || args.filename || "";
221
+ if (filePath) {
222
+ const ext = path.extname(filePath).toLowerCase();
223
+ if (ext) observation.signals.fileExtensions.push(ext);
224
+ }
225
+
226
+ // Count thinking blocks
227
+ if (msg.type === "thinking" || msg.role === "thinking") {
228
+ observation.signals.thinkingBlocks++;
229
+ }
230
+
231
+ // Token usage
232
+ if (msg.usage) {
233
+ if (!observation.signals.tokenUsage) {
234
+ observation.signals.tokenUsage = { input: 0, output: 0, total: 0 };
235
+ }
236
+ observation.signals.tokenUsage.input += msg.usage.input_tokens || msg.usage.input || 0;
237
+ observation.signals.tokenUsage.output += msg.usage.output_tokens || msg.usage.output || 0;
238
+ observation.signals.tokenUsage.total += msg.usage.total_tokens || msg.usage.totalTokens || 0;
239
+ }
240
+ }
241
+ }
242
+
243
+ // Codex: notification format
244
+ if (data.type === "agent-turn-complete") {
245
+ observation.signals.messageCount = (data["input-messages"] || []).length;
246
+ }
247
+
248
+ // Gemini: AfterAgent / SessionEnd
249
+ if (data.toolResults || data.toolCalls) {
250
+ const tools = data.toolCalls || data.toolResults || [];
251
+ for (const t of tools) {
252
+ observation.signals.toolsUsed.push(t.name || t.toolName);
253
+ }
254
+ }
255
+
256
+ // Deduplicate
257
+ observation.signals.toolsUsed = [...new Set(observation.signals.toolsUsed.filter(Boolean))];
258
+ observation.signals.fileExtensions = [...new Set(observation.signals.fileExtensions.filter(Boolean))];
259
+
260
+ // Skip empty observations
261
+ if (
262
+ observation.signals.toolsUsed.length === 0 &&
263
+ observation.signals.messageCount === 0 &&
264
+ !observation.signals.tokenUsage
265
+ ) {
266
+ return null;
267
+ }
268
+
269
+ return observation;
270
+ }
271
+
272
+ function detectAgent() {
273
+ // Detect from environment or parent process
274
+ if (process.env.CLAUDE_CODE) return "claude-code";
275
+ if (process.env.CURSOR_SESSION) return "cursor";
276
+ if (process.env.CODEX_HOME) return "codex";
277
+ if (process.env.GEMINI_PROJECT_DIR) return "gemini";
278
+ return "unknown";
279
+ }
280
+
281
+ function appendObservation(obs) {
282
+ try {
283
+ const raw = fs.readFileSync(OBSERVATIONS_FILE, "utf-8");
284
+ const data = JSON.parse(raw);
285
+ data.sessions.push(obs);
286
+ data.accumulated.totalSessions++;
287
+
288
+ // Update tool counts
289
+ for (const tool of obs.signals.toolsUsed) {
290
+ data.accumulated.toolCounts[tool] = (data.accumulated.toolCounts[tool] || 0) + 1;
291
+ }
292
+
293
+ // Update token totals
294
+ if (obs.signals.tokenUsage) {
295
+ data.accumulated.totalTokens += obs.signals.tokenUsage.total || 0;
296
+ }
297
+
298
+ // Keep only last 500 detailed sessions (rolling window)
299
+ if (data.sessions.length > 500) {
300
+ data.sessions = data.sessions.slice(-500);
301
+ }
302
+
303
+ fs.writeFileSync(OBSERVATIONS_FILE, JSON.stringify(data, null, 2) + "\\n");
304
+ } catch (err) {
305
+ // If file is corrupted, start fresh
306
+ fs.writeFileSync(OBSERVATIONS_FILE, JSON.stringify({
307
+ version: 1,
308
+ sessions: [obs],
309
+ accumulated: {
310
+ totalSessions: 1,
311
+ totalTokens: obs.signals.tokenUsage?.total || 0,
312
+ toolCounts: Object.fromEntries(obs.signals.toolsUsed.map(t => [t, 1])),
313
+ domainSignals: [],
314
+ lastSubmitted: null,
315
+ },
316
+ }, null, 2) + "\\n");
317
+ }
318
+ }
319
+ `;
320
+ }
321
+ // ── Hook Installers ────────────────────────────────────────────────────
322
+ function writeObserverScript() {
323
+ ensureObserverDirs();
324
+ const scriptPath = join(HOOKS_DIR, "observer.js");
325
+ writeFileSync(scriptPath, getObserverScript());
326
+ chmodSync(scriptPath, 0o755);
327
+ return scriptPath;
328
+ }
329
+ /**
330
+ * Install hook for Claude Code (~/.claude/hooks.json)
331
+ * Uses the Stop event to observe after each session turn.
332
+ */
333
+ function installClaudeCodeHook(configDir, scriptPath) {
334
+ const hookFile = join(configDir, "hooks.json");
335
+ let config = {};
336
+ if (existsSync(hookFile)) {
337
+ try {
338
+ config = JSON.parse(readFileSync(hookFile, "utf-8"));
339
+ }
340
+ catch {
341
+ config = {};
342
+ }
343
+ }
344
+ // Ensure hooks object exists
345
+ if (!config.hooks)
346
+ config.hooks = {};
347
+ const hooks = config.hooks;
348
+ // Add to Stop event (don't duplicate)
349
+ if (!hooks.Stop)
350
+ hooks.Stop = [];
351
+ const stopHooks = hooks.Stop;
352
+ if (!stopHooks.some((h) => h.command?.includes("jobarbiter"))) {
353
+ stopHooks.push({
354
+ command: `node ${scriptPath}`,
355
+ timeout: 10,
356
+ });
357
+ }
358
+ mkdirSync(configDir, { recursive: true });
359
+ writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
360
+ }
361
+ /**
362
+ * Install hook for Cursor (~/.cursor/hooks.json)
363
+ * Same JSON format as Claude Code — uses stop event.
364
+ */
365
+ function installCursorHook(configDir, scriptPath) {
366
+ const hookFile = join(configDir, "hooks.json");
367
+ let config = {};
368
+ if (existsSync(hookFile)) {
369
+ try {
370
+ config = JSON.parse(readFileSync(hookFile, "utf-8"));
371
+ }
372
+ catch {
373
+ config = {};
374
+ }
375
+ }
376
+ if (!config.version)
377
+ config.version = 1;
378
+ if (!config.hooks)
379
+ config.hooks = {};
380
+ const hooks = config.hooks;
381
+ // Use stop event
382
+ if (!hooks.stop)
383
+ hooks.stop = [];
384
+ const stopHooks = hooks.stop;
385
+ if (!stopHooks.some((h) => h.command?.includes("jobarbiter"))) {
386
+ stopHooks.push({
387
+ command: `node ${scriptPath}`,
388
+ timeout: 10,
389
+ });
390
+ }
391
+ mkdirSync(configDir, { recursive: true });
392
+ writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
393
+ }
394
+ /**
395
+ * Install plugin for OpenCode (~/.config/opencode/plugins/)
396
+ * OpenCode uses JS/TS plugin modules, not JSON config.
397
+ */
398
+ function installOpenCodeHook(configDir, scriptPath) {
399
+ const pluginDir = join(configDir, "plugins");
400
+ mkdirSync(pluginDir, { recursive: true });
401
+ const pluginCode = `// JobArbiter Observer Plugin for OpenCode
402
+ // Observes session activity and extracts proficiency signals.
403
+
404
+ const { execSync } = require("child_process");
405
+ const { readFileSync } = require("fs");
406
+
407
+ exports.JobArbiterObserver = async ({ project, client, $, directory }) => {
408
+ return {
409
+ event: async ({ event }) => {
410
+ if (event.type === "session.idle" || event.type === "session.updated") {
411
+ try {
412
+ const sessionData = JSON.stringify({
413
+ sessionId: event.sessionId || "unknown",
414
+ agent: "opencode",
415
+ messages: event.messages || [],
416
+ });
417
+ execSync(\`echo '\${sessionData.replace(/'/g, "'\\"'\\"\\'")}' | node ${scriptPath}\`, {
418
+ stdio: "ignore",
419
+ timeout: 10000,
420
+ env: { ...process.env, JOBARBITER_AGENT: "opencode" },
421
+ });
422
+ } catch {
423
+ // Silent failure
424
+ }
425
+ }
426
+ },
427
+ "tool.execute.after": async (input, output) => {
428
+ // Track individual tool executions for richer signal
429
+ try {
430
+ const toolData = JSON.stringify({
431
+ agent: "opencode",
432
+ messages: [{
433
+ tool_name: input.tool,
434
+ args: output.args || {},
435
+ }],
436
+ });
437
+ execSync(\`echo '\${toolData.replace(/'/g, "'\\"'\\"\\'")}' | node ${scriptPath}\`, {
438
+ stdio: "ignore",
439
+ timeout: 5000,
440
+ env: { ...process.env, JOBARBITER_AGENT: "opencode" },
441
+ });
442
+ } catch {
443
+ // Silent failure
444
+ }
445
+ },
446
+ };
447
+ };
448
+ `;
449
+ writeFileSync(join(pluginDir, "jobarbiter-observer.js"), pluginCode);
450
+ }
451
+ /**
452
+ * Install notification handler for Codex CLI (~/.codex/config.toml)
453
+ * Codex uses a `notify` config key that runs an external program.
454
+ */
455
+ function installCodexHook(configDir, scriptPath) {
456
+ const configFile = join(configDir, "config.toml");
457
+ // Create a wrapper script that converts Codex's CLI arg format to stdin
458
+ const wrapperPath = join(HOOKS_DIR, "codex-wrapper.sh");
459
+ writeFileSync(wrapperPath, `#!/bin/bash
460
+ # JobArbiter Observer wrapper for Codex CLI
461
+ # Codex passes JSON as $1, our observer reads stdin
462
+ echo "$1" | JOBARBITER_AGENT=codex node ${scriptPath}
463
+ `);
464
+ chmodSync(wrapperPath, 0o755);
465
+ mkdirSync(configDir, { recursive: true });
466
+ let config = "";
467
+ if (existsSync(configFile)) {
468
+ config = readFileSync(configFile, "utf-8");
469
+ }
470
+ // Add notify line if not present
471
+ if (!config.includes("jobarbiter")) {
472
+ // Check if there's already a notify line
473
+ if (config.includes("notify")) {
474
+ // Don't overwrite existing notify — append comment
475
+ config += `\n# JobArbiter observer: add this to your notify script:\n# ${wrapperPath} "$1"\n`;
476
+ }
477
+ else {
478
+ config += `\n# JobArbiter observer\nnotify = "${wrapperPath}"\n`;
479
+ }
480
+ writeFileSync(configFile, config);
481
+ }
482
+ }
483
+ /**
484
+ * Install hook for Gemini CLI (~/.gemini/settings.json)
485
+ * Uses SessionEnd event.
486
+ */
487
+ function installGeminiHook(configDir, scriptPath) {
488
+ const settingsFile = join(configDir, "settings.json");
489
+ let settings = {};
490
+ if (existsSync(settingsFile)) {
491
+ try {
492
+ settings = JSON.parse(readFileSync(settingsFile, "utf-8"));
493
+ }
494
+ catch {
495
+ settings = {};
496
+ }
497
+ }
498
+ if (!settings.hooks)
499
+ settings.hooks = {};
500
+ const hooks = settings.hooks;
501
+ // Add SessionEnd hook
502
+ if (!hooks.SessionEnd)
503
+ hooks.SessionEnd = [];
504
+ const sessionEndHooks = hooks.SessionEnd;
505
+ if (!sessionEndHooks.some((h) => h.hooks?.some((hh) => hh.command?.includes("jobarbiter")))) {
506
+ sessionEndHooks.push({
507
+ matcher: "*",
508
+ hooks: [
509
+ {
510
+ name: "jobarbiter-observer",
511
+ type: "command",
512
+ command: `node ${scriptPath}`,
513
+ timeout: 10000,
514
+ },
515
+ ],
516
+ });
517
+ }
518
+ mkdirSync(configDir, { recursive: true });
519
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
520
+ }
521
+ // ── Public API ─────────────────────────────────────────────────────────
522
+ /**
523
+ * Install observer hooks for the specified agents.
524
+ * Returns a summary of what was installed.
525
+ */
526
+ export function installObservers(agentIds) {
527
+ const scriptPath = writeObserverScript();
528
+ const result = {
529
+ installed: [],
530
+ skipped: [],
531
+ errors: [],
532
+ };
533
+ for (const agentId of agentIds) {
534
+ const def = AGENT_DEFINITIONS.find((d) => d.id === agentId);
535
+ if (!def) {
536
+ result.errors.push({ agent: agentId, error: "Unknown agent" });
537
+ continue;
538
+ }
539
+ // Check if already installed
540
+ if (isHookInstalled(def.id, def.configDir, def.hookFormat)) {
541
+ result.skipped.push(def.name);
542
+ continue;
543
+ }
544
+ try {
545
+ switch (def.hookFormat) {
546
+ case "claude":
547
+ installClaudeCodeHook(def.configDir, scriptPath);
548
+ break;
549
+ case "cursor":
550
+ installCursorHook(def.configDir, scriptPath);
551
+ break;
552
+ case "opencode":
553
+ installOpenCodeHook(def.configDir, scriptPath);
554
+ break;
555
+ case "codex":
556
+ installCodexHook(def.configDir, scriptPath);
557
+ break;
558
+ case "gemini":
559
+ installGeminiHook(def.configDir, scriptPath);
560
+ break;
561
+ }
562
+ result.installed.push(def.name);
563
+ }
564
+ catch (err) {
565
+ result.errors.push({
566
+ agent: def.name,
567
+ error: err instanceof Error ? err.message : String(err),
568
+ });
569
+ }
570
+ }
571
+ return result;
572
+ }
573
+ /**
574
+ * Remove observer hooks for the specified agents.
575
+ */
576
+ export function removeObservers(agentIds) {
577
+ const result = { removed: [], notFound: [] };
578
+ for (const agentId of agentIds) {
579
+ const def = AGENT_DEFINITIONS.find((d) => d.id === agentId);
580
+ if (!def) {
581
+ result.notFound.push(agentId);
582
+ continue;
583
+ }
584
+ try {
585
+ switch (def.hookFormat) {
586
+ case "claude":
587
+ case "cursor": {
588
+ const hookFile = join(def.configDir, "hooks.json");
589
+ if (existsSync(hookFile)) {
590
+ const config = JSON.parse(readFileSync(hookFile, "utf-8"));
591
+ for (const [key, hooks] of Object.entries(config.hooks || {})) {
592
+ if (Array.isArray(hooks)) {
593
+ config.hooks[key] = hooks.filter((h) => !JSON.stringify(h).includes("jobarbiter"));
594
+ }
595
+ }
596
+ writeFileSync(hookFile, JSON.stringify(config, null, 2) + "\n");
597
+ result.removed.push(def.name);
598
+ }
599
+ else {
600
+ result.notFound.push(def.name);
601
+ }
602
+ break;
603
+ }
604
+ case "opencode": {
605
+ const pluginFile = join(def.configDir, "plugins", "jobarbiter-observer.js");
606
+ if (existsSync(pluginFile)) {
607
+ unlinkSync(pluginFile);
608
+ result.removed.push(def.name);
609
+ }
610
+ else {
611
+ result.notFound.push(def.name);
612
+ }
613
+ break;
614
+ }
615
+ case "codex": {
616
+ const configFile = join(def.configDir, "config.toml");
617
+ if (existsSync(configFile)) {
618
+ let content = readFileSync(configFile, "utf-8");
619
+ content = content
620
+ .split("\n")
621
+ .filter((line) => !line.includes("jobarbiter"))
622
+ .join("\n");
623
+ writeFileSync(configFile, content);
624
+ result.removed.push(def.name);
625
+ }
626
+ else {
627
+ result.notFound.push(def.name);
628
+ }
629
+ break;
630
+ }
631
+ case "gemini": {
632
+ const settingsFile = join(def.configDir, "settings.json");
633
+ if (existsSync(settingsFile)) {
634
+ const settings = JSON.parse(readFileSync(settingsFile, "utf-8"));
635
+ for (const [key, hookGroups] of Object.entries(settings.hooks || {})) {
636
+ if (Array.isArray(hookGroups)) {
637
+ settings.hooks[key] = hookGroups.filter((g) => !JSON.stringify(g).includes("jobarbiter"));
638
+ }
639
+ }
640
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
641
+ result.removed.push(def.name);
642
+ }
643
+ else {
644
+ result.notFound.push(def.name);
645
+ }
646
+ break;
647
+ }
648
+ }
649
+ }
650
+ catch {
651
+ result.notFound.push(def.name);
652
+ }
653
+ }
654
+ return result;
655
+ }
656
+ /**
657
+ * Get observation status — what's been accumulated locally.
658
+ */
659
+ export function getObservationStatus() {
660
+ ensureObserverDirs();
661
+ try {
662
+ const raw = readFileSync(OBSERVATIONS_FILE, "utf-8");
663
+ const data = JSON.parse(raw);
664
+ const topTools = Object.entries(data.accumulated?.toolCounts || {})
665
+ .map(([tool, count]) => ({ tool, count: count }))
666
+ .sort((a, b) => b.count - a.count)
667
+ .slice(0, 10);
668
+ const agents = [
669
+ ...new Set((data.sessions || []).map((s) => s.agent).filter(Boolean)),
670
+ ];
671
+ return {
672
+ hasData: (data.accumulated?.totalSessions || 0) > 0,
673
+ totalSessions: data.accumulated?.totalSessions || 0,
674
+ totalTokens: data.accumulated?.totalTokens || 0,
675
+ topTools,
676
+ agents,
677
+ lastSubmitted: data.accumulated?.lastSubmitted || null,
678
+ };
679
+ }
680
+ catch {
681
+ return {
682
+ hasData: false,
683
+ totalSessions: 0,
684
+ totalTokens: 0,
685
+ topTools: [],
686
+ agents: [],
687
+ lastSubmitted: null,
688
+ };
689
+ }
690
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Interactive Onboard Wizard
3
+ *
4
+ * A polished, step-by-step wizard that guides users through:
5
+ * - Account creation (email + verification)
6
+ * - User type selection (worker vs employer)
7
+ * - Profile setup (track, tools, domains)
8
+ * - Optional integrations (GitHub)
9
+ */
10
+ export declare function runOnboardWizard(opts: {
11
+ force?: boolean;
12
+ baseUrl?: string;
13
+ }): Promise<void>;