hivenet 0.6.0 → 0.7.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,1005 @@
1
+ import {
2
+ attachmentFlags
3
+ } from "./chunk-OEHURQBM.js";
4
+ import {
5
+ ATTACHMENT_TARGET_TYPES,
6
+ EVAL_STATUSES,
7
+ EVENT_STATUSES,
8
+ FEEDBACK_CATEGORIES,
9
+ FeedbackError,
10
+ ISSUE_STATUSES,
11
+ MAX_TRAJECTORY_BYTES,
12
+ MAX_TRIALS_PER_PUSH,
13
+ SECRET_ENV_NAME,
14
+ SECRET_VALUE_PATTERNS,
15
+ __toESM,
16
+ attachmentTargetSchema,
17
+ ownerApi,
18
+ require_arg,
19
+ resolveApiOrigin
20
+ } from "./chunk-A3UY57EQ.js";
21
+
22
+ // src/owner.ts
23
+ import { chmodSync, mkdirSync, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync } from "fs";
24
+ import path2 from "path";
25
+ var import_arg = __toESM(require_arg(), 1);
26
+
27
+ // src/ci-context.ts
28
+ import { readFileSync } from "fs";
29
+ var CHECKOUT_EVENTS = /* @__PURE__ */ new Set(["push", "schedule", "workflow_dispatch", "merge_group"]);
30
+ var str = (value) => typeof value === "string" && value ? value : void 0;
31
+ function ciContext(env = process.env) {
32
+ if (env.GITHUB_ACTIONS !== "true" || !env.GITHUB_REPOSITORY) return void 0;
33
+ let pullRequest;
34
+ try {
35
+ const event = env.GITHUB_EVENT_PATH ? JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, "utf8")) : void 0;
36
+ pullRequest = event?.pull_request;
37
+ } catch {
38
+ pullRequest = void 0;
39
+ }
40
+ const checkedOut = CHECKOUT_EVENTS.has(env.GITHUB_EVENT_NAME ?? "");
41
+ const commit = pullRequest ? str(pullRequest.head?.sha) : checkedOut ? env.GITHUB_SHA : void 0;
42
+ const branch = pullRequest ? str(pullRequest.head?.ref) || env.GITHUB_HEAD_REF : checkedOut ? env.GITHUB_REF_NAME : void 0;
43
+ const server = env.GITHUB_SERVER_URL || "https://github.com";
44
+ return {
45
+ provider: "github",
46
+ repository: env.GITHUB_REPOSITORY,
47
+ ...commit && /^[0-9a-f]{7,64}$/i.test(commit) ? { commit } : {},
48
+ ...branch ? { branch } : {},
49
+ ...typeof pullRequest?.number === "number" ? { pull_request: pullRequest.number } : {},
50
+ ...env.GITHUB_EVENT_NAME ? { event: env.GITHUB_EVENT_NAME } : {},
51
+ ...env.GITHUB_RUN_ID ? { run_url: `${server}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` } : {}
52
+ };
53
+ }
54
+
55
+ // src/harbor-job.ts
56
+ import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
57
+ import path from "path";
58
+ function readJson(file) {
59
+ try {
60
+ const value = JSON.parse(readFileSync2(file, "utf8"));
61
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function str2(value) {
67
+ return typeof value === "string" && value ? value : void 0;
68
+ }
69
+ function num(value) {
70
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
71
+ }
72
+ function seconds(timing) {
73
+ const t = timing;
74
+ const start = str2(t?.started_at);
75
+ const end = str2(t?.finished_at);
76
+ if (!start || !end) return void 0;
77
+ const ms = Date.parse(end) - Date.parse(start);
78
+ return Number.isFinite(ms) && ms >= 0 ? Math.round(ms / 100) / 10 : void 0;
79
+ }
80
+ function rewardCriteria(node, out = {}) {
81
+ if (!node || typeof node !== "object") return out;
82
+ for (const [key, value] of Object.entries(node)) {
83
+ if (key === "criteria" && Array.isArray(value)) {
84
+ for (const criterion of value) {
85
+ const id = str2(criterion?.id) ?? str2(criterion?.name);
86
+ const score = num(criterion?.value);
87
+ if (id && score !== void 0) out[id] = score;
88
+ }
89
+ } else {
90
+ rewardCriteria(value, out);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+ function rewardsOf(result) {
96
+ const raw = result.verifier_result?.rewards;
97
+ if (!raw || typeof raw !== "object") return void 0;
98
+ const rewards = Object.fromEntries(
99
+ Object.entries(raw).filter((entry) => num(entry[1]) !== void 0)
100
+ );
101
+ return Object.keys(rewards).length ? rewards : void 0;
102
+ }
103
+ function list(value) {
104
+ return Array.isArray(value) ? value : [];
105
+ }
106
+ function scenarioOf(config, lock) {
107
+ const agent = config.agent ?? {};
108
+ const environment = config.environment ?? {};
109
+ const mounts = list(environment.mounts).map((mount) => str2(mount?.source)).filter((source) => !!source).map((source) => path.basename(path.resolve(source)));
110
+ const skills = list(lock?.skills).map((skill) => str2(skill?.name)).filter((name) => !!name);
111
+ const mcpServers = list(agent.mcp_servers).map((server) => str2(server?.name)).filter((name) => !!name);
112
+ const instructions = [
113
+ ...list(config.extra_instructions).map(str2),
114
+ ...list(config.extra_instruction_paths).map(
115
+ (file) => str2(file) ? path.basename(str2(file)) : void 0
116
+ )
117
+ ].filter((text) => !!text).map((text) => text.slice(0, 500));
118
+ const options = {};
119
+ for (const [key, value] of [
120
+ ...Object.entries(agent.kwargs ?? {}),
121
+ ...Object.entries(agent.env ?? {})
122
+ ]) {
123
+ if (key === "reasoning_effort" || SECRET_ENV_NAME.test(key)) continue;
124
+ if (!["string", "number", "boolean"].includes(typeof value)) continue;
125
+ options[key.slice(0, 64)] = String(value).slice(0, 200);
126
+ }
127
+ const scenario = {
128
+ ...mounts.length ? { mounts: mounts.slice(0, 5) } : {},
129
+ ...skills.length ? { skills: skills.slice(0, 20) } : {},
130
+ ...mcpServers.length ? { mcp_servers: mcpServers.slice(0, 20) } : {},
131
+ ...instructions.length ? { instructions: instructions.slice(0, 5) } : {},
132
+ ...Object.keys(options).length ? { agent_options: Object.fromEntries(Object.entries(options).slice(0, 20)) } : {}
133
+ };
134
+ return Object.keys(scenario).length ? scenario : void 0;
135
+ }
136
+ function readTrial(dir, job) {
137
+ const result = readJson(path.join(dir, "result.json"));
138
+ if (!result) return { reason: "no result.json yet (still running?)" };
139
+ const lock = readJson(path.join(dir, "lock.json"));
140
+ const digest = str2(lock?.task?.digest);
141
+ if (!digest) return { reason: "no task digest in lock.json" };
142
+ const config = result.config ?? {};
143
+ const agentInfo = result.agent_info ?? {};
144
+ const modelInfo = agentInfo.model_info ?? null;
145
+ const agentResult = result.agent_result ?? {};
146
+ const exception = result.exception_info ?? null;
147
+ const source = config.source_trial ?? null;
148
+ const jobId = str2(config.job_id);
149
+ const id = str2(result.id);
150
+ const taskName = str2(result.task_name);
151
+ if (!id || !jobId || !taskName) return { reason: "result.json lacks the trial, job or task id" };
152
+ const model = str2(modelInfo?.name) ?? str2(config.agent?.model_name);
153
+ const provider = str2(modelInfo?.provider);
154
+ const effort = str2(
155
+ config.agent?.kwargs?.reasoning_effort
156
+ );
157
+ const scenario = scenarioOf(config, lock);
158
+ const rewards = rewardsOf(result);
159
+ const details = readJson(path.join(dir, "verifier", "reward-details.json"));
160
+ const metrics = Object.fromEntries(
161
+ Object.entries({
162
+ input_tokens: num(agentResult.n_input_tokens),
163
+ output_tokens: num(agentResult.n_output_tokens),
164
+ cache_tokens: num(agentResult.n_cache_tokens),
165
+ cost_usd: num(agentResult.cost_usd),
166
+ setup_sec: seconds(result.environment_setup),
167
+ agent_sec: seconds(result.agent_execution),
168
+ verifier_sec: seconds(result.verifier)
169
+ }).filter((entry) => entry[1] !== void 0)
170
+ );
171
+ return {
172
+ harbor_trial_id: id,
173
+ harbor_job_id: jobId,
174
+ harbor_job_name: job.name,
175
+ ...str2(source?.trial_id) ? { source_harbor_trial_id: str2(source?.trial_id) } : {},
176
+ task_name: taskName,
177
+ task_digest: digest,
178
+ agent: str2(agentInfo.name) ?? "unknown",
179
+ ...str2(agentInfo.version) ? { agent_version: str2(agentInfo.version) } : {},
180
+ ...model ? { model: provider && !model.includes("/") ? `${provider}/${model}` : model } : {},
181
+ ...effort ? { effort } : {},
182
+ ...job.harbor_version ? { harbor_version: job.harbor_version } : {},
183
+ ...exception ? {
184
+ exception: {
185
+ type: str2(exception.exception_type) ?? "UnknownError",
186
+ message: (str2(exception.exception_message) ?? "").slice(0, 2e3)
187
+ }
188
+ } : {},
189
+ ...rewards ? { rewards, criteria: rewardCriteria(details) } : {},
190
+ ...Object.keys(metrics).length ? { metrics } : {},
191
+ ...str2(result.started_at) ? { started_at: str2(result.started_at) } : {},
192
+ ...str2(result.finished_at) ? { finished_at: str2(result.finished_at) } : {},
193
+ ...scenario ? { scenario } : {}
194
+ };
195
+ }
196
+ function readHarborJob(jobDir) {
197
+ if (!existsSync(jobDir) || !statSync(jobDir).isDirectory()) {
198
+ throw new Error(`${jobDir} is not a directory.`);
199
+ }
200
+ const harborVersion = str2(readJson(path.join(jobDir, "lock.json"))?.harbor?.version);
201
+ const job = {
202
+ name: path.basename(path.resolve(jobDir)),
203
+ ...harborVersion ? { harbor_version: harborVersion } : {}
204
+ };
205
+ const read = { job, trials: [], trajectories: {}, unreadable: [] };
206
+ for (const entry of readdirSync(jobDir, { withFileTypes: true })) {
207
+ if (!entry.isDirectory()) continue;
208
+ const dir = path.join(jobDir, entry.name);
209
+ if (!existsSync(path.join(dir, "result.json")) && !existsSync(path.join(dir, "lock.json"))) {
210
+ continue;
211
+ }
212
+ const trial = readTrial(dir, job);
213
+ if ("reason" in trial) {
214
+ read.unreadable.push({ trial: entry.name, reason: trial.reason });
215
+ continue;
216
+ }
217
+ read.trials.push(trial);
218
+ const trajectory = path.join(dir, "agent", "trajectory.json");
219
+ if (existsSync(trajectory)) read.trajectories[trial.harbor_trial_id.toLowerCase()] = trajectory;
220
+ }
221
+ return read;
222
+ }
223
+
224
+ // src/owner.ts
225
+ var SECRET_ENV_VAR = "HIVENET_SECRET_KEY";
226
+ async function runAttach(argv) {
227
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, ...attachmentFlags() }, { argv });
228
+ if (args["--help"]) {
229
+ console.log(`
230
+ Usage
231
+ $ npx hivenet attach <record type> <id> --attach <path> [--describe "<what it shows>"] \u2026
232
+
233
+ Record types: ${ATTACHMENT_TARGET_TYPES.join(" | ")}.
234
+ Up to 10 files per record, 20 MiB each. Files on a question are shared with every agent
235
+ that receives it; everything else stays private. Auth: --secret or ${SECRET_ENV_VAR}.
236
+ `);
237
+ return;
238
+ }
239
+ const target = attachmentTargetSchema.safeParse({ type: args._[0], id: args._[1] });
240
+ if (!target.success || args._.length !== 2 || !args["--attach"]?.length) {
241
+ throw new FeedbackError(
242
+ "Usage: hivenet attach <record type> <id> --attach <path>. See hivenet attach --help."
243
+ );
244
+ }
245
+ const api = await buildApi(args);
246
+ const result = await api.attachFiles(target.data, args["--attach"]);
247
+ console.log(
248
+ args["--json"] ? JSON.stringify(result, null, 2) : `\u2713 Attached ${args["--attach"].length} file(s) to ${target.data.type} ${target.data.id}.`
249
+ );
250
+ }
251
+ var OWNER_FLAGS = {
252
+ "--to": String,
253
+ "--dsn": String,
254
+ "--api": String,
255
+ "--registry": String,
256
+ "--secret": String,
257
+ "--json": Boolean,
258
+ "--help": Boolean,
259
+ "-h": "--help"
260
+ };
261
+ async function buildApi(args) {
262
+ const secretKey = args["--secret"] ?? process.env[SECRET_ENV_VAR];
263
+ if (!secretKey?.startsWith("hv_sec_")) {
264
+ throw new FeedbackError(
265
+ `An owner secret key is required (hv_sec_\u2026, from your project's Settings).
266
+ Pass --secret <key> or set ${SECRET_ENV_VAR}.`
267
+ );
268
+ }
269
+ const apiOrigin = args["--api"] ?? await resolveApiOrigin({
270
+ ...args["--to"] !== void 0 ? { to: args["--to"] } : {},
271
+ ...args["--dsn"] !== void 0 ? { dsn: args["--dsn"] } : {},
272
+ ...args["--registry"] !== void 0 ? { registry: args["--registry"] } : {}
273
+ });
274
+ return {
275
+ ...ownerApi({ secretKey, apiOrigin }),
276
+ // Copy the destination into next-step hints, never the owner's credential.
277
+ command: (subcommand) => `npx hivenet ${subcommand} --api ${JSON.stringify(apiOrigin)}`
278
+ };
279
+ }
280
+ function statusIcon(status) {
281
+ return { new: "\u25CF", seen: "\u25D0", in_progress: "\u25D2", resolved: "\u2713", wont_fix: "\u2717", spam: "\u2298" }[status];
282
+ }
283
+ function printEvent(event, verbose = false) {
284
+ const subject = event.subject ? ` \xB7 ${event.subject}` : "";
285
+ console.log(
286
+ `${statusIcon(event.status)} ${event.id} [${event.status}] ${event.category}${subject} (${event.agent ?? event.author ?? "unknown"}, ${event.received_at.slice(0, 16).replace("T", " ")})`
287
+ );
288
+ console.log(` ${event.feedback}`);
289
+ if (event.triage_note) {
290
+ console.log(` \u21B3 note: ${event.triage_note}`);
291
+ }
292
+ if (verbose) {
293
+ console.log(
294
+ ` thread ${event.thread_id} \xB7 telemetry ${event.consent_telemetry ? "on" : "off"}`
295
+ );
296
+ }
297
+ }
298
+ async function runInbox(argv) {
299
+ const args = (0, import_arg.default)(
300
+ {
301
+ ...OWNER_FLAGS,
302
+ "--status": String,
303
+ "--category": String,
304
+ "--thread": String,
305
+ "--limit": Number
306
+ },
307
+ { argv }
308
+ );
309
+ if (args["--help"]) {
310
+ console.log(`
311
+ Usage
312
+ $ npx hivenet inbox [--status new] [--category mcp] [--thread <id>] [--limit 50]
313
+
314
+ List feedback for your project (owners only). Auth: --secret <hv_sec_\u2026> or
315
+ ${SECRET_ENV_VAR}. Destination: --to <slug|domain>, --dsn, HIVENET_DSN, or --api <origin>
316
+ (or --registry / HIVENET_REGISTRY for your instance). Statuses: ${EVENT_STATUSES.join(" | ")}. --json for raw output.
317
+ `);
318
+ return;
319
+ }
320
+ const status = args["--status"];
321
+ if (status && !EVENT_STATUSES.includes(status)) {
322
+ throw new FeedbackError(
323
+ `Invalid --status "${status}". Expected: ${EVENT_STATUSES.join(", ")}.`
324
+ );
325
+ }
326
+ const api = await buildApi(args);
327
+ const result = await api.listFeedback({
328
+ ...status ? { status } : {},
329
+ ...args["--category"] ? { category: args["--category"] } : {},
330
+ ...args["--thread"] ? { thread: args["--thread"] } : {},
331
+ ...args["--limit"] ? { limit: args["--limit"] } : {}
332
+ });
333
+ if (args["--json"]) {
334
+ console.log(JSON.stringify(result, null, 2));
335
+ return;
336
+ }
337
+ if (result.events.length === 0) {
338
+ console.log(`No matching feedback for ${result.project}.`);
339
+ return;
340
+ }
341
+ console.log(`${result.events.length} event(s) for ${result.project}:
342
+ `);
343
+ for (const event of result.events) {
344
+ printEvent(event);
345
+ }
346
+ console.log(
347
+ `
348
+ Triage: ${api.command("triage")} <eventId> --status <${EVENT_STATUSES.join("|")}> [--note "\u2026"]`
349
+ );
350
+ }
351
+ async function runTriage(argv) {
352
+ const args = (0, import_arg.default)(
353
+ { ...OWNER_FLAGS, "--status": String, "--note": String, "--link": String },
354
+ { argv }
355
+ );
356
+ if (args["--help"] || args._.length === 0) {
357
+ console.log(`
358
+ Usage
359
+ $ npx hivenet triage <eventId> [--status <status>] [--note "why"] [--link <url>]
360
+
361
+ Set triage status and/or attach an external link (Slack, Linear, GitHub, PR) to a
362
+ feedback event (owners only). Statuses: ${EVENT_STATUSES.join(" | ")}.
363
+ Auth: --secret <hv_sec_\u2026> or ${SECRET_ENV_VAR}. Destination: --to/--dsn/--api.
364
+ `);
365
+ if (!args["--help"]) {
366
+ throw new FeedbackError("An event ID is required.");
367
+ }
368
+ return;
369
+ }
370
+ const status = args["--status"];
371
+ if (status && !EVENT_STATUSES.includes(status)) {
372
+ throw new FeedbackError(`Invalid --status. Expected: ${EVENT_STATUSES.join(", ")}.`);
373
+ }
374
+ if (!status && !args["--link"]) {
375
+ throw new FeedbackError("Provide --status and/or --link.");
376
+ }
377
+ const api = await buildApi(args);
378
+ const id = args._[0];
379
+ if (args["--link"]) {
380
+ await api.addLink(id, args["--link"]);
381
+ }
382
+ const event = status ? (await api.setFeedbackStatus(
383
+ id,
384
+ status,
385
+ args["--note"] !== void 0 ? args["--note"] : void 0
386
+ )).event : (await api.getFeedback(id)).event;
387
+ if (args["--json"]) {
388
+ console.log(JSON.stringify({ event }, null, 2));
389
+ return;
390
+ }
391
+ console.log("Updated:");
392
+ printEvent(event, true);
393
+ printLinks(event.links_json);
394
+ }
395
+ function printLinks(linksJson) {
396
+ try {
397
+ const links = JSON.parse(linksJson ?? "[]");
398
+ for (const link of links) {
399
+ console.log(` \u21C4 ${link.kind}: ${link.url}`);
400
+ }
401
+ } catch {
402
+ }
403
+ }
404
+ function issueIcon(status) {
405
+ return {
406
+ open: "\u25CF",
407
+ in_progress: "\u25D2",
408
+ needs_review: "\u25CE",
409
+ resolved: "\u2713",
410
+ wont_fix: "\u2717",
411
+ spam: "\u2298"
412
+ }[status];
413
+ }
414
+ function printIssue(issue) {
415
+ const regressed = issue.reopened_count > 0 ? ` \u21BB regressed \xD7${issue.reopened_count}` : "";
416
+ console.log(
417
+ `${issueIcon(issue.status)} ${issue.id} [${issue.status}] ${issue.title} (${issue.event_count} event${issue.event_count === 1 ? "" : "s"}, last ${issue.last_seen.slice(0, 16).replace("T", " ")})${regressed}`
418
+ );
419
+ printLinks(issue.links_json);
420
+ }
421
+ async function runIssues(argv) {
422
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--status": String, "--limit": Number }, { argv });
423
+ if (args["--help"]) {
424
+ console.log(`
425
+ Usage
426
+ $ npx hivenet issues [--status open] [--limit 50]
427
+
428
+ List issues \u2014 feedback clustered by thread and (category, subject) fingerprint.
429
+ Statuses: ${ISSUE_STATUSES.join(" | ")}. Auth: --secret or ${SECRET_ENV_VAR}.
430
+ Manage one: npx hivenet issue <issueId> --status resolved --note "\u2026" --link <url>
431
+ `);
432
+ return;
433
+ }
434
+ const status = args["--status"];
435
+ if (status && !ISSUE_STATUSES.includes(status)) {
436
+ throw new FeedbackError(`Invalid --status. Expected: ${ISSUE_STATUSES.join(", ")}.`);
437
+ }
438
+ const api = await buildApi(args);
439
+ const result = await api.listIssues({
440
+ ...status ? { status } : {},
441
+ ...args["--limit"] ? { limit: args["--limit"] } : {}
442
+ });
443
+ if (args["--json"]) {
444
+ console.log(JSON.stringify(result, null, 2));
445
+ return;
446
+ }
447
+ if (result.issues.length === 0) {
448
+ console.log(`No matching issues for ${result.project}.`);
449
+ return;
450
+ }
451
+ console.log(`${result.issues.length} issue(s) for ${result.project}:
452
+ `);
453
+ for (const issue of result.issues) {
454
+ printIssue(issue);
455
+ }
456
+ }
457
+ async function runIssue(argv) {
458
+ const args = (0, import_arg.default)(
459
+ { ...OWNER_FLAGS, "--status": String, "--note": String, "--link": String },
460
+ { argv }
461
+ );
462
+ if (args["--help"] || args._.length === 0) {
463
+ console.log(`
464
+ Usage
465
+ $ npx hivenet issue <issueId> (show with events)
466
+ $ npx hivenet issue <issueId> --status resolved --note "\u2026" --link <url>
467
+
468
+ Statuses: ${ISSUE_STATUSES.join(" | ")}. Resolving cascades to member events; new
469
+ feedback on a resolved issue reopens it. Auth: --secret or ${SECRET_ENV_VAR}.
470
+ `);
471
+ if (!args["--help"]) {
472
+ throw new FeedbackError("An issue ID is required.");
473
+ }
474
+ return;
475
+ }
476
+ const status = args["--status"];
477
+ if (status && !ISSUE_STATUSES.includes(status)) {
478
+ throw new FeedbackError(`Invalid --status. Expected: ${ISSUE_STATUSES.join(", ")}.`);
479
+ }
480
+ if (args["--note"] !== void 0 && !status) {
481
+ throw new FeedbackError("--note requires --status.");
482
+ }
483
+ const api = await buildApi(args);
484
+ const id = args._[0];
485
+ if (args["--link"]) {
486
+ await api.addLink(id, args["--link"]);
487
+ }
488
+ if (status) {
489
+ await api.setIssueStatus(id, status, args["--note"] !== void 0 ? args["--note"] : void 0);
490
+ }
491
+ const { issue, events } = await api.getIssue(id);
492
+ if (args["--json"]) {
493
+ console.log(JSON.stringify({ issue, events }, null, 2));
494
+ return;
495
+ }
496
+ printIssue(issue);
497
+ if (issue.summary) {
498
+ console.log(` ${issue.summary}`);
499
+ }
500
+ console.log("");
501
+ for (const event of events) {
502
+ printEvent(event);
503
+ }
504
+ }
505
+ function printQuestion(question) {
506
+ const icon = question.status === "active" ? "\u25CF" : "\u2713";
507
+ const auto = question.source === "system" ? " [auto]" : "";
508
+ const target = question.target.category ? ` \xB7 ${question.target.category}` : question.target.issue_id ? ` \xB7 verifies ${question.target.issue_id.slice(0, 14)}\u2026` : "";
509
+ console.log(`${icon} ${question.id} [${question.status}]${auto}${target}`);
510
+ console.log(` ${question.prompt}`);
511
+ for (const answer of question.answers) {
512
+ const who = answer.agent ?? answer.client_name ?? "unknown";
513
+ console.log(
514
+ ` \u21B3 (${who}, ${answer.received_at.slice(0, 16).replace("T", " ")}) ${answer.feedback}`
515
+ );
516
+ }
517
+ }
518
+ async function runQuestions(argv) {
519
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--status": String }, { argv });
520
+ if (args["--help"]) {
521
+ console.log(`
522
+ Usage
523
+ $ npx hivenet questions [--status active|closed] (list with answers)
524
+ $ npx hivenet ask "<prompt>" [--category cli] (publish a question)
525
+ $ npx hivenet question <id> --close (stop delivery)
526
+
527
+ Questions ride along with submission responses \u2014 one per response, delivered
528
+ to matching agents; answers group under each question. Auth: --secret or
529
+ ${SECRET_ENV_VAR}.
530
+
531
+ Show agents what you mean: --attach <path> [--describe "<what it shows>"] on ask
532
+ (repeatable; 10 files, 20 MiB each). Every agent the question reaches can open them.
533
+ `);
534
+ return;
535
+ }
536
+ const status = args["--status"];
537
+ if (status && status !== "active" && status !== "closed") {
538
+ throw new FeedbackError("Invalid --status. Expected: active or closed.");
539
+ }
540
+ const api = await buildApi(args);
541
+ const result = await api.listQuestions(status ? { status } : {});
542
+ if (args["--json"]) {
543
+ console.log(JSON.stringify(result, null, 2));
544
+ return;
545
+ }
546
+ if (result.questions.length === 0) {
547
+ console.log(`No questions yet. Publish one: ${api.command("ask")} "<prompt>" [--category cli]`);
548
+ return;
549
+ }
550
+ console.log(`${result.questions.length} question(s):
551
+ `);
552
+ for (const question of result.questions) {
553
+ printQuestion(question);
554
+ console.log("");
555
+ }
556
+ }
557
+ async function runAsk(argv) {
558
+ const args = (0, import_arg.default)(
559
+ { ...OWNER_FLAGS, "--category": String, "--idempotency-key": String, ...attachmentFlags() },
560
+ { argv }
561
+ );
562
+ const prompt = args._.join(" ").trim();
563
+ if (args["--help"] || prompt.length === 0) {
564
+ console.log(`
565
+ Usage
566
+ $ npx hivenet ask "If you used <feature>: what broke or was missing?" [--category cli]
567
+
568
+ Publishes a question to the agents using your product. Targeted beats
569
+ open-ended. Categories: ${FEEDBACK_CATEGORIES.join(" | ")}. Auth: --secret or ${SECRET_ENV_VAR}.
570
+ --attach <path> [--describe "<what it shows>"] shares a file with every agent that receives it.
571
+ --idempotency-key <key> publishes the question once however often you retry (8\u201364 letters,
572
+ digits, _ or -).
573
+ `);
574
+ if (!args["--help"]) {
575
+ throw new FeedbackError("A question prompt is required.");
576
+ }
577
+ return;
578
+ }
579
+ const api = await buildApi(args);
580
+ const { question } = await api.createQuestion(prompt, args["--category"], {
581
+ ...args["--attach"] ? { files: args["--attach"] } : {},
582
+ ...args["--idempotency-key"] ? { idempotencyKey: args["--idempotency-key"] } : {}
583
+ });
584
+ if (args["--json"]) {
585
+ console.log(JSON.stringify({ question }, null, 2));
586
+ return;
587
+ }
588
+ console.log(`\u2713 Question published (${question.id})`);
589
+ console.log(` It rides along with matching submission responses \u2014 one ask per response.`);
590
+ console.log(
591
+ ` Answers: ${api.command("questions")} \xB7 close: ${api.command("question")} ${question.id} --close`
592
+ );
593
+ }
594
+ async function runQuestion(argv) {
595
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--close": Boolean }, { argv });
596
+ if (args["--help"] || args._.length === 0) {
597
+ console.log(`
598
+ Usage
599
+ $ npx hivenet question <questionId> (show with answers)
600
+ $ npx hivenet question <questionId> --close (stop delivery)
601
+ `);
602
+ if (!args["--help"]) {
603
+ throw new FeedbackError("A question ID is required.");
604
+ }
605
+ return;
606
+ }
607
+ const api = await buildApi(args);
608
+ const id = args._[0];
609
+ if (args["--close"]) {
610
+ await api.closeQuestion(id);
611
+ }
612
+ const { questions } = await api.listQuestions();
613
+ const question = questions.find((candidate) => candidate.id === id);
614
+ if (!question) {
615
+ throw new FeedbackError(`Question "${id}" not found.`);
616
+ }
617
+ if (args["--json"]) {
618
+ console.log(JSON.stringify({ question }, null, 2));
619
+ return;
620
+ }
621
+ printQuestion(question);
622
+ }
623
+ function evalIcon(status) {
624
+ return { proposed: "\u25C7", accepted: "\u25C6", archived: "\u229F" }[status];
625
+ }
626
+ function printEval(entry) {
627
+ console.log(`${evalIcon(entry.status)} ${entry.id} [${entry.status}] ${entry.title}`);
628
+ console.log(` task: ${entry.task}`);
629
+ if (entry.expected) console.log(` expected: ${entry.expected}`);
630
+ if (entry.setup) {
631
+ console.log(` setup: ${entry.setup}`);
632
+ } else {
633
+ console.log(
634
+ ` setup: missing${entry.setup_pending ? " \u2014 reporter asked on their thread" : ""}`
635
+ );
636
+ }
637
+ if (entry.source) {
638
+ const who = entry.source.agent ?? entry.source.client_name ?? "unknown";
639
+ const attempts = entry.source.attempts ? ` \xB7 ${entry.source.attempts} attempts` : "";
640
+ console.log(` from ${entry.source.event_id} (${who}${attempts})`);
641
+ }
642
+ if (entry.note) console.log(` \u21B3 note: ${entry.note}`);
643
+ const version = entry.current_version ?? entry.latest_version;
644
+ if (version) {
645
+ const results = entry.results;
646
+ const runs = results?.graded ? ` \xB7 ${results.passed}/${results.graded} passed` : results?.errored ? ` \xB7 ${results.errored} errored` : " \xB7 no runs yet";
647
+ const validation = results?.validation.status ?? "unvalidated";
648
+ console.log(
649
+ ` runnable: v${version.number} ${entry.current_version ? "approved" : "draft"} \xB7 ${validation}${runs}`
650
+ );
651
+ }
652
+ }
653
+ async function runEvals(argv) {
654
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--status": String }, { argv });
655
+ if (args["--help"]) {
656
+ console.log(`
657
+ Usage
658
+ $ npx hivenet evals [--status proposed|accepted|archived] (list the corpus)
659
+ $ npx hivenet eval <id> [--status accepted] [--note "\u2026"] (curate one)
660
+ $ npx hivenet promote <eventId> (report \u2192 eval)
661
+
662
+ Evals are real tasks agents failed at, promoted from failed-task reports.
663
+ Use --json for a machine-readable corpus export. Auth: --secret or ${SECRET_ENV_VAR}.
664
+ `);
665
+ return;
666
+ }
667
+ const status = args["--status"];
668
+ if (status && !EVAL_STATUSES.includes(status)) {
669
+ throw new FeedbackError(`Invalid --status. Expected: ${EVAL_STATUSES.join(", ")}.`);
670
+ }
671
+ const api = await buildApi(args);
672
+ const result = await api.listEvals(status ? { status } : {});
673
+ if (args["--json"]) {
674
+ console.log(JSON.stringify(result, null, 2));
675
+ return;
676
+ }
677
+ if (result.evals.length === 0) {
678
+ console.log(`No evals yet. Promote a failed-task report: ${api.command("promote")} <eventId>`);
679
+ return;
680
+ }
681
+ console.log(`${result.evals.length} eval(s):
682
+ `);
683
+ for (const entry of result.evals) {
684
+ printEval(entry);
685
+ console.log("");
686
+ }
687
+ }
688
+ async function runEval(argv) {
689
+ if (argv[0] === "draft") return runEvalDraft(argv.slice(1));
690
+ if (argv[0] === "pull") return runEvalPull(argv.slice(1));
691
+ if (argv[0] === "push") return runEvalPush(argv.slice(1));
692
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--status": String, "--note": String }, { argv });
693
+ if (args["--help"] || args._.length === 0) {
694
+ console.log(`
695
+ Usage
696
+ $ npx hivenet eval <evalId> (show)
697
+ $ npx hivenet eval <evalId> --status accepted (curate)
698
+ $ npx hivenet eval <evalId> --status archived --note "why"
699
+ $ npx hivenet eval draft <evalId> --file version.json (draft a runnable version)
700
+ $ npx hivenet eval pull [evalId[@N] \u2026] --harbor <dir> (write Harbor tasks)
701
+ $ npx hivenet eval push <harbor job dir> \u2026 (send results back)
702
+
703
+ Statuses: ${EVAL_STATUSES.join(" | ")}. Auth: --secret or ${SECRET_ENV_VAR}.
704
+ `);
705
+ if (!args["--help"]) {
706
+ throw new FeedbackError("An eval ID is required.");
707
+ }
708
+ return;
709
+ }
710
+ const status = args["--status"];
711
+ if (status && !EVAL_STATUSES.includes(status)) {
712
+ throw new FeedbackError(`Invalid --status. Expected: ${EVAL_STATUSES.join(", ")}.`);
713
+ }
714
+ if (args["--note"] !== void 0 && !status) {
715
+ throw new FeedbackError("--note requires --status.");
716
+ }
717
+ const api = await buildApi(args);
718
+ const id = args._[0];
719
+ if (status) {
720
+ await api.setEvalStatus(id, status, args["--note"] !== void 0 ? args["--note"] : void 0);
721
+ }
722
+ const { eval: entry } = await api.getEval(id);
723
+ if (args["--json"]) {
724
+ console.log(JSON.stringify({ eval: entry }, null, 2));
725
+ return;
726
+ }
727
+ printEval(entry);
728
+ }
729
+ async function runPromote(argv) {
730
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS }, { argv });
731
+ if (args["--help"] || args._.length === 0) {
732
+ console.log(`
733
+ Usage
734
+ $ npx hivenet promote <eventId> (failed-task report \u2192 eval candidate)
735
+
736
+ Idempotent per event. When the report lacks reproduction state, the reporter
737
+ is asked for it on their thread; the answer fills the eval's setup.
738
+ `);
739
+ if (!args["--help"]) {
740
+ throw new FeedbackError("An event ID is required.");
741
+ }
742
+ return;
743
+ }
744
+ const api = await buildApi(args);
745
+ const { eval: entry, created } = await api.promoteEval(args._[0]);
746
+ if (args["--json"]) {
747
+ console.log(JSON.stringify({ eval: entry, created }, null, 2));
748
+ return;
749
+ }
750
+ console.log(created ? `\u2713 Promoted to eval (${entry.id})` : `Already promoted (${entry.id})`);
751
+ printEval(entry);
752
+ }
753
+ var TEMPLATE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-[^}]*)?\}/g;
754
+ function hostEnv(tasks) {
755
+ const required = /* @__PURE__ */ new Set();
756
+ const optional = /* @__PURE__ */ new Set();
757
+ for (const task of tasks) {
758
+ for (const match of (task.files["task.toml"] ?? "").matchAll(TEMPLATE)) {
759
+ (match[2] ? optional : required).add(match[1]);
760
+ }
761
+ }
762
+ for (const name of required) optional.delete(name);
763
+ return { required: [...required].sort(), optional: [...optional].sort() };
764
+ }
765
+ async function runEvalPull(argv) {
766
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--harbor": String }, { argv });
767
+ if (args["--help"]) {
768
+ console.log(`
769
+ Usage
770
+ $ npx hivenet eval pull --harbor <dir> (every accepted eval, current version)
771
+ $ npx hivenet eval pull <evalId> [<evalId>@N \u2026] --harbor <dir>
772
+
773
+ Writes each eval's compiled Harbor task to <dir>/<evalId>. A ref without @N takes the current
774
+ version, else the newest draft; @N picks any version, so a draft can be validated before a
775
+ person approves it in the dashboard. Pulling again replaces <dir>/<evalId>.
776
+ Auth: --secret or ${SECRET_ENV_VAR}.
777
+ `);
778
+ return;
779
+ }
780
+ const dir = args["--harbor"];
781
+ if (!dir) {
782
+ throw new FeedbackError(
783
+ "Usage: hivenet eval pull [evalId[@N] \u2026] --harbor <dir>. See hivenet eval pull --help."
784
+ );
785
+ }
786
+ const api = await buildApi(args);
787
+ const { tasks, missing } = await api.pullEvalTasks(args._);
788
+ const root = path2.resolve(dir);
789
+ for (const task of tasks) {
790
+ if (!/^ev_[a-z0-9]+$/.test(task.eval_id)) {
791
+ throw new FeedbackError(`Refusing to write a task named ${JSON.stringify(task.eval_id)}.`);
792
+ }
793
+ const target = path2.join(root, task.eval_id);
794
+ rmSync(target, { recursive: true, force: true });
795
+ for (const [file, text] of Object.entries(task.files)) {
796
+ const full = path2.resolve(target, file);
797
+ if (!full.startsWith(`${target}${path2.sep}`)) {
798
+ throw new FeedbackError(`Refusing to write ${file} outside ${target}.`);
799
+ }
800
+ mkdirSync(path2.dirname(full), { recursive: true });
801
+ writeFileSync(full, text);
802
+ if (file.endsWith(".sh")) chmodSync(full, 493);
803
+ }
804
+ }
805
+ if (args["--json"]) {
806
+ console.log(
807
+ JSON.stringify(
808
+ {
809
+ dir: root,
810
+ tasks: tasks.map(({ files: _files, ...task }) => task),
811
+ missing,
812
+ env: hostEnv(tasks)
813
+ },
814
+ null,
815
+ 2
816
+ )
817
+ );
818
+ return;
819
+ }
820
+ if (!tasks.length) {
821
+ console.log(
822
+ args._.length ? "Nothing to pull." : "No approved versions yet. Pull a draft by id (hivenet eval pull ev_\u2026@N --harbor <dir>), or approve one in the dashboard."
823
+ );
824
+ }
825
+ for (const task of tasks) {
826
+ console.log(
827
+ `\u2713 ${task.eval_id} v${task.version.number} (${task.version.status}) \u2192 ${path2.join(dir, task.eval_id)} ${task.title}`
828
+ );
829
+ }
830
+ for (const ref of missing) console.log(`\u2717 ${ref}: no such eval or version`);
831
+ if (missing.length) process.exitCode = 1;
832
+ if (!tasks.length) return;
833
+ const env = hostEnv(tasks);
834
+ const hasSolution = tasks.some((task) => "solution/solve.sh" in task.files);
835
+ console.log(`
836
+ Next
837
+ ${hasSolution ? ` harbor run -p ${dir} -a oracle (validate: the reference solution passes)
838
+ ` : ""} harbor run -p ${dir} -a nop (validate: doing nothing fails)
839
+ harbor run -p ${dir} -a claude-code -m anthropic/claude-sonnet-5 -k 3
840
+ hivenet eval push jobs/<job name>`);
841
+ if (env.required.length) console.log(`
842
+ Set on this machine: ${env.required.join(", ")}`);
843
+ if (env.optional.length) console.log(` Optional (defaulted): ${env.optional.join(", ")}`);
844
+ }
845
+ function keepLocal(file) {
846
+ const size = statSync2(file).size;
847
+ if (size > MAX_TRAJECTORY_BYTES) {
848
+ return { reason: `over ${MAX_TRAJECTORY_BYTES / 1024 / 1024} MB` };
849
+ }
850
+ const text = readFileSync3(file, "utf8");
851
+ const named = Object.entries(process.env).find(
852
+ ([name, value]) => SECRET_ENV_NAME.test(name) && value && value.length >= 8 && text.includes(value)
853
+ );
854
+ if (named) return { reason: `it contains the value of ${named[0]}` };
855
+ const secret = SECRET_VALUE_PATTERNS.find(([pattern]) => pattern.test(text));
856
+ return secret ? { reason: `it contains what looks like ${secret[1]}` } : { text };
857
+ }
858
+ async function runEvalPush(argv) {
859
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--no-trajectories": Boolean }, { argv });
860
+ if (args["--help"] || !args._.length) {
861
+ console.log(`
862
+ Usage
863
+ $ npx hivenet eval push <harbor job dir> [<job dir> \u2026] [--no-trajectories]
864
+
865
+ Sends each trial's result and grade back to Hivenet: which version it ran (by the task digest
866
+ in lock.json), the agent, model and scenario, the reward and every criterion, cost and time.
867
+ Then uploads each trial's trajectory (agent/trajectory.json) so the dashboard and your agents
868
+ can read why it failed; --no-trajectories keeps them here. In GitHub Actions it also records
869
+ the repository, commit, branch, pull request and workflow run. Logs stay on this machine.
870
+ Pushing a job twice stores only what's new. Auth: --secret or ${SECRET_ENV_VAR}.
871
+ `);
872
+ if (!args["--help"]) throw new FeedbackError("A Harbor job directory is required.");
873
+ return;
874
+ }
875
+ const api = await buildApi(args);
876
+ const ci = ciContext();
877
+ const reports = [];
878
+ const empty = [];
879
+ for (const jobDir of args._) {
880
+ const read = readHarborJob(jobDir);
881
+ if (!read.trials.length) {
882
+ empty.push(jobDir);
883
+ process.exitCode = 1;
884
+ continue;
885
+ }
886
+ const summary = {
887
+ stored: 0,
888
+ duplicates: 0,
889
+ skipped: [],
890
+ trajectories: 0,
891
+ kept_local: [],
892
+ runs: []
893
+ };
894
+ for (let start = 0; start < read.trials.length; start += MAX_TRIALS_PER_PUSH) {
895
+ const pushed = await api.pushEvalResults(
896
+ read.trials.slice(start, start + MAX_TRIALS_PER_PUSH).map((trial) => ({ ...trial, ci }))
897
+ );
898
+ summary.stored += pushed.stored;
899
+ summary.duplicates += pushed.duplicates;
900
+ summary.skipped.push(...pushed.skipped);
901
+ for (const run of pushed.runs) {
902
+ if (!summary.runs.some((known) => known.job_id === run.job_id)) summary.runs.push(run);
903
+ }
904
+ if (args["--no-trajectories"]) continue;
905
+ for (const id of pushed.trajectories) {
906
+ const file = read.trajectories[id];
907
+ if (!file) continue;
908
+ const upload = keepLocal(file);
909
+ if ("reason" in upload) {
910
+ summary.kept_local.push({ harbor_trial_id: id, reason: upload.reason });
911
+ continue;
912
+ }
913
+ try {
914
+ if ((await api.pushTrialTrajectory(id, upload.text)).stored) summary.trajectories += 1;
915
+ } catch (error) {
916
+ summary.kept_local.push({
917
+ harbor_trial_id: id,
918
+ reason: error instanceof Error ? error.message : String(error)
919
+ });
920
+ }
921
+ }
922
+ }
923
+ reports.push({
924
+ job: jobDir,
925
+ trials: read.trials.length,
926
+ unreadable: read.unreadable,
927
+ ...summary
928
+ });
929
+ }
930
+ if (args["--json"]) {
931
+ console.log(JSON.stringify({ jobs: reports, ...empty.length ? { empty } : {} }, null, 2));
932
+ return;
933
+ }
934
+ for (const dir of empty) {
935
+ console.log(`\u2717 ${dir}: no finished trials here. Pass a job folder, jobs/<job name>.`);
936
+ }
937
+ for (const report of reports) {
938
+ const dupes = report.duplicates ? `, ${report.duplicates} already stored` : "";
939
+ const trajectories = report.trajectories ? `, ${report.trajectories} trajectories` : "";
940
+ console.log(`\u2713 ${report.job}: ${report.stored} trial(s) stored${dupes}${trajectories}.`);
941
+ for (const run of report.runs) console.log(` \u2192 ${run.url}`);
942
+ for (const skipped of report.skipped) {
943
+ console.log(` skipped ${skipped.task_name}: ${skipped.reason}`);
944
+ }
945
+ for (const kept of report.kept_local) {
946
+ console.log(` trajectory of ${kept.harbor_trial_id} kept here: ${kept.reason}`);
947
+ }
948
+ for (const unreadable of report.unreadable) {
949
+ console.log(` unreadable ${unreadable.trial}: ${unreadable.reason}`);
950
+ }
951
+ }
952
+ }
953
+ async function runEvalDraft(argv) {
954
+ const args = (0, import_arg.default)({ ...OWNER_FLAGS, "--file": String }, { argv });
955
+ if (args["--help"] || args._.length !== 1 || !args["--file"]) {
956
+ console.log(`
957
+ Usage
958
+ $ npx hivenet eval draft <evalId> --file version.json (or --file - to read stdin)
959
+
960
+ Drafts a runnable version: instruction, criteria, environment and an optional solution, as
961
+ JSON (the draft_eval_version MCP tool takes the same fields; evals.md shows an example).
962
+ Hivenet compiles it into a Harbor task and prints lint warnings. Identical fields return the
963
+ existing version. A person approves it in the dashboard. Auth: --secret or ${SECRET_ENV_VAR}.
964
+ `);
965
+ if (!args["--help"]) {
966
+ throw new FeedbackError("Usage: hivenet eval draft <evalId> --file version.json.");
967
+ }
968
+ return;
969
+ }
970
+ let fields;
971
+ try {
972
+ const text = args["--file"] === "-" ? readFileSync3(0, "utf8") : readFileSync3(args["--file"], "utf8");
973
+ fields = JSON.parse(text);
974
+ } catch (error) {
975
+ throw new FeedbackError(
976
+ `Could not read ${args["--file"]} as JSON (${error instanceof Error ? error.message : String(error)}).`
977
+ );
978
+ }
979
+ const api = await buildApi(args);
980
+ const { version, created } = await api.draftEvalVersion(args._[0], fields);
981
+ if (args["--json"]) {
982
+ console.log(JSON.stringify({ version, created }, null, 2));
983
+ return;
984
+ }
985
+ console.log(
986
+ `${created ? "\u2713 Drafted" : "= Unchanged,"} ${args._[0]} v${version.number} (${version.status}) \xB7 ${version.digest.slice(0, 19)}\u2026`
987
+ );
988
+ for (const warning of version.warnings) console.log(` \u26A0 ${warning}`);
989
+ console.log(
990
+ ` Validate it: hivenet eval pull ${args._[0]}@${version.number} --harbor <dir>, then harbor run -a oracle and -a nop.`
991
+ );
992
+ }
993
+ export {
994
+ runAsk,
995
+ runAttach,
996
+ runEval,
997
+ runEvals,
998
+ runInbox,
999
+ runIssue,
1000
+ runIssues,
1001
+ runPromote,
1002
+ runQuestion,
1003
+ runQuestions,
1004
+ runTriage
1005
+ };