@arcadeai/hypotenuse 0.15.0 → 0.16.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.
@@ -343,7 +343,7 @@ import { Command } from "commander";
343
343
  // package.json
344
344
  var package_default = {
345
345
  name: "@arcadeai/hypotenuse",
346
- version: "0.15.0",
346
+ version: "0.16.1",
347
347
  description: "Hypotenuse \u2014 run coding agents on your machine for Linear agent sessions",
348
348
  type: "module",
349
349
  license: "Apache-2.0",
@@ -2493,7 +2493,7 @@ function applyOverrides(config, overrides, env = process.env) {
2493
2493
  },
2494
2494
  log: {
2495
2495
  ...config.log,
2496
- level: overrides.verbose ? "debug" : envLevel && LOG_LEVELS.includes(envLevel) ? envLevel : config.log.level
2496
+ level: overrides.logLevel ?? (overrides.verbose ? "debug" : envLevel && LOG_LEVELS.includes(envLevel) ? envLevel : config.log.level)
2497
2497
  }
2498
2498
  };
2499
2499
  }
@@ -4037,6 +4037,124 @@ function changedFiles(cwd) {
4037
4037
  }
4038
4038
  }
4039
4039
 
4040
+ // src/daemon/log.ts
4041
+ import {
4042
+ appendFileSync as appendFileSync2,
4043
+ chmodSync as chmodSync2,
4044
+ existsSync as existsSync7,
4045
+ mkdirSync as mkdirSync3,
4046
+ renameSync as renameSync2,
4047
+ rmSync as rmSync2,
4048
+ statSync as statSync3
4049
+ } from "fs";
4050
+ import { dirname as dirname5 } from "path";
4051
+ var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
4052
+ var LOG_KEEP = 5;
4053
+ var RANK = {
4054
+ error: 0,
4055
+ warn: 1,
4056
+ info: 2,
4057
+ debug: 3,
4058
+ trace: 4
4059
+ };
4060
+ var LEVEL_TOKEN = {
4061
+ error: "status-error",
4062
+ warn: "status-waiting",
4063
+ info: "status-active",
4064
+ debug: "text-tertiary",
4065
+ trace: "text-tertiary"
4066
+ };
4067
+ var LINE = /^(\S+) (error|warn |info |debug|trace) (.*)$/;
4068
+ var FIELD_KEY = / ([A-Za-z][\w.]*)=/g;
4069
+ function paintLogLine(line, mode = getMode()) {
4070
+ if (mode.depth === "none")
4071
+ return line;
4072
+ const match = LINE.exec(line);
4073
+ if (!match)
4074
+ return line;
4075
+ const [, time = "", padded = "", rest = ""] = match;
4076
+ const level = padded.trim();
4077
+ const { color, dim } = createTheme(mode);
4078
+ const token = LEVEL_TOKEN[level];
4079
+ const body = rest.replace(FIELD_KEY, (_, key) => ` ${dim(`${key}=`)}`);
4080
+ const message = level === "error" || level === "warn" ? color(token, body) : level === "debug" || level === "trace" ? dim(body) : body;
4081
+ return `${dim(time)} ${color(token, padded)} ${message}`;
4082
+ }
4083
+ var PREVIEW_LIMIT = 200;
4084
+ function preview(text, level) {
4085
+ const line = text.replace(/\s+/g, " ").trim();
4086
+ return level !== "trace" && line.length > PREVIEW_LIMIT ? `${line.slice(0, PREVIEW_LIMIT - 1)}\u2026` : line;
4087
+ }
4088
+
4089
+ class DaemonLog {
4090
+ file;
4091
+ level;
4092
+ echo;
4093
+ paint;
4094
+ bytes = 0;
4095
+ constructor(file, level, echo, paint = Boolean(process.stderr.isTTY) || process.argv.includes("--force-color") || isTruthyEnv(process.env.FORCE_COLOR)) {
4096
+ this.file = file;
4097
+ this.level = level;
4098
+ this.echo = echo;
4099
+ this.paint = paint;
4100
+ if (file) {
4101
+ mkdirSync3(dirname5(file), { recursive: true, mode: 448 });
4102
+ this.bytes = existsSync7(file) ? statSync3(file).size : 0;
4103
+ }
4104
+ }
4105
+ enabled(level) {
4106
+ return RANK[level] <= RANK[this.level];
4107
+ }
4108
+ write(level, message, fields = {}) {
4109
+ if (!this.enabled(level))
4110
+ return;
4111
+ const extra = Object.entries(fields).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${typeof v === "string" && /\s/.test(v) ? JSON.stringify(v) : v}`).join(" ");
4112
+ const line = `${new Date().toISOString()} ${level.padEnd(5)} ${message}${extra ? ` ${extra}` : ""}
4113
+ `;
4114
+ if (this.echo)
4115
+ process.stderr.write(this.paint ? `${paintLogLine(line.slice(0, -1))}
4116
+ ` : line);
4117
+ if (!this.file)
4118
+ return;
4119
+ try {
4120
+ if (this.bytes + line.length > LOG_ROTATE_BYTES)
4121
+ this.rotate();
4122
+ const fresh = !existsSync7(this.file);
4123
+ appendFileSync2(this.file, line, { mode: 384 });
4124
+ if (fresh)
4125
+ chmodSync2(this.file, 384);
4126
+ this.bytes += Buffer.byteLength(line);
4127
+ } catch {}
4128
+ }
4129
+ rotate() {
4130
+ if (!this.file)
4131
+ return;
4132
+ rmSync2(`${this.file}.${LOG_KEEP}`, { force: true });
4133
+ for (let i = LOG_KEEP - 1;i >= 1; i--) {
4134
+ if (existsSync7(`${this.file}.${i}`))
4135
+ renameSync2(`${this.file}.${i}`, `${this.file}.${i + 1}`);
4136
+ }
4137
+ if (existsSync7(this.file))
4138
+ renameSync2(this.file, `${this.file}.1`);
4139
+ this.bytes = 0;
4140
+ }
4141
+ error(message, fields) {
4142
+ this.write("error", message, fields);
4143
+ }
4144
+ warn(message, fields) {
4145
+ this.write("warn", message, fields);
4146
+ }
4147
+ info(message, fields) {
4148
+ this.write("info", message, fields);
4149
+ }
4150
+ debug(message, fields) {
4151
+ this.write("debug", message, fields);
4152
+ }
4153
+ trace(message, fields) {
4154
+ this.write("trace", message, fields);
4155
+ }
4156
+ }
4157
+
4040
4158
  // src/daemon/main.ts
4041
4159
  import { spawn as spawn2 } from "child_process";
4042
4160
  import { createHash as createHash2 } from "crypto";
@@ -4044,7 +4162,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync10, rmSync as r
4044
4162
  import { join as join10 } from "path";
4045
4163
 
4046
4164
  // src/acp/host.ts
4047
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
4165
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
4048
4166
  import { join as join8 } from "path";
4049
4167
 
4050
4168
  // src/telemetry/acp.ts
@@ -4431,7 +4549,7 @@ class NativeReceiver {
4431
4549
  }
4432
4550
 
4433
4551
  // src/worktree/co-author.ts
4434
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
4552
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "fs";
4435
4553
  import { join as join5 } from "path";
4436
4554
  var GIT_HOOK_NAMES = [
4437
4555
  "applypatch-msg",
@@ -4477,13 +4595,13 @@ exit 0
4477
4595
  `;
4478
4596
  function installCoAuthorHooks(stateDir) {
4479
4597
  const dir = join5(stateDir, "git-hooks");
4480
- mkdirSync3(dir, { recursive: true, mode: 448 });
4598
+ mkdirSync4(dir, { recursive: true, mode: 448 });
4481
4599
  for (const name of GIT_HOOK_NAMES) {
4482
4600
  const path = join5(dir, name);
4483
4601
  const temporary = `${path}.${process.pid}.tmp`;
4484
4602
  writeFileSync2(temporary, HOOK_SCRIPT, { mode: 493 });
4485
- chmodSync2(temporary, 493);
4486
- renameSync2(temporary, path);
4603
+ chmodSync3(temporary, 493);
4604
+ renameSync3(temporary, path);
4487
4605
  }
4488
4606
  return dir;
4489
4607
  }
@@ -4500,7 +4618,7 @@ function coAuthorEnvironment(env, hooksDir, coAuthor) {
4500
4618
  }
4501
4619
 
4502
4620
  // src/worktree/manager.ts
4503
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
4621
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
4504
4622
  import { join as join6 } from "path";
4505
4623
 
4506
4624
  // src/worktree/branch.ts
@@ -4526,12 +4644,12 @@ function issueDirectory(identifier, fallback) {
4526
4644
  }
4527
4645
 
4528
4646
  // src/worktree/disk.ts
4529
- import { existsSync as existsSync7, statfsSync } from "fs";
4530
- import { dirname as dirname5 } from "path";
4647
+ import { existsSync as existsSync8, statfsSync } from "fs";
4648
+ import { dirname as dirname6 } from "path";
4531
4649
  function freeBytes(path, statfs = statfsSync) {
4532
4650
  let current = path;
4533
- while (!existsSync7(current)) {
4534
- const parent = dirname5(current);
4651
+ while (!existsSync8(current)) {
4652
+ const parent = dirname6(current);
4535
4653
  if (parent === current)
4536
4654
  break;
4537
4655
  current = parent;
@@ -4600,7 +4718,7 @@ function stderrTail(result, lines = 20) {
4600
4718
  }
4601
4719
 
4602
4720
  // src/worktree/lock.ts
4603
- import { existsSync as existsSync8, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4721
+ import { existsSync as existsSync9, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
4604
4722
 
4605
4723
  class KeyedMutex {
4606
4724
  tails = new Map;
@@ -4631,7 +4749,7 @@ function pidAlive(pid) {
4631
4749
  }
4632
4750
  }
4633
4751
  function readCheckoutLock(file) {
4634
- if (!existsSync8(file))
4752
+ if (!existsSync9(file))
4635
4753
  return null;
4636
4754
  try {
4637
4755
  return JSON.parse(readFileSync5(file, "utf8"));
@@ -4654,7 +4772,7 @@ function takeCheckoutLock(file, agentSessionId, pid = process.pid) {
4654
4772
  function releaseCheckoutLock(file, agentSessionId) {
4655
4773
  const held = readCheckoutLock(file);
4656
4774
  if (!held || held.agentSessionId === agentSessionId)
4657
- rmSync2(file, { force: true });
4775
+ rmSync3(file, { force: true });
4658
4776
  }
4659
4777
 
4660
4778
  // src/worktree/setup.ts
@@ -4669,7 +4787,8 @@ function runSetupCommand(command, options) {
4669
4787
  const started = now();
4670
4788
  const tail = [];
4671
4789
  return new Promise((resolve) => {
4672
- const child = spawn("sh", ["-c", command], {
4790
+ const child = spawn("sh", ["-c", `exec 2>&1
4791
+ ${command}`], {
4673
4792
  cwd: options.cwd,
4674
4793
  env: options.env,
4675
4794
  stdio: ["ignore", "pipe", "pipe"],
@@ -4823,7 +4942,7 @@ class WorktreeManager {
4823
4942
  this.refuseIfShort(request);
4824
4943
  await this.fetchBase(request, repository, baseBranch);
4825
4944
  const path = this.worktreePath(request, repository);
4826
- mkdirSync4(join6(path, ".."), { recursive: true, mode: 448 });
4945
+ mkdirSync5(join6(path, ".."), { recursive: true, mode: 448 });
4827
4946
  await this.git(["worktree", "prune"], { cwd: repository.path });
4828
4947
  await this.must(["worktree", "add", "--detach", path, `origin/${baseBranch}`], repository.path, `create a read-only checkout of ${baseBranch}`);
4829
4948
  const created = this.state.insertWorktree({
@@ -4870,7 +4989,7 @@ class WorktreeManager {
4870
4989
  async scratch(request) {
4871
4990
  const id = request.assignment.agentSessionId;
4872
4991
  const path = join6(request.config.sessions.worktree_root, SCRATCH, String(id));
4873
- mkdirSync4(path, { recursive: true, mode: 448 });
4992
+ mkdirSync5(path, { recursive: true, mode: 448 });
4874
4993
  const known = this.state.worktreeAt(path);
4875
4994
  let row;
4876
4995
  if (known && known.state !== "removed") {
@@ -4950,7 +5069,7 @@ ${stderrTail(fetched)}
4950
5069
  }
4951
5070
  fetchAge(repoPath) {
4952
5071
  try {
4953
- const age = this.now() - statSync3(join6(repoPath, ".git", "FETCH_HEAD")).mtimeMs;
5072
+ const age = this.now() - statSync4(join6(repoPath, ".git", "FETCH_HEAD")).mtimeMs;
4954
5073
  if (age < 60000)
4955
5074
  return null;
4956
5075
  const units = [
@@ -5007,7 +5126,7 @@ ${stderrTail(fetched)}
5007
5126
  for (let n = 1;; n++) {
5008
5127
  const path = join6(parent, suffixed(dir, n));
5009
5128
  const row = this.state.worktreeAt(path);
5010
- if (!existsSync9(path) && (!row || row.state === "removed"))
5129
+ if (!existsSync10(path) && (!row || row.state === "removed"))
5011
5130
  return path;
5012
5131
  }
5013
5132
  }
@@ -5062,7 +5181,7 @@ ${stderrTail(fetched)}
5062
5181
  const reachable = await this.fetchBase(request, repository, baseBranch);
5063
5182
  const { branch, local, remote } = await this.chooseBranch(request, repository, repoConfig, reachable);
5064
5183
  const path = this.worktreePath(request, repository);
5065
- mkdirSync4(join6(path, ".."), { recursive: true, mode: 448 });
5184
+ mkdirSync5(join6(path, ".."), { recursive: true, mode: 448 });
5066
5185
  await this.git(["worktree", "prune"], { cwd: repository.path });
5067
5186
  let add;
5068
5187
  if (local) {
@@ -5106,7 +5225,7 @@ ${stderrTail(fetched)}
5106
5225
  reusable(agentSessionId) {
5107
5226
  const session = this.state.session(agentSessionId);
5108
5227
  const row = session?.worktreeId ? this.state.worktree(session.worktreeId) : null;
5109
- if (!row || row.kind !== "worktree" || row.state === "removed" || !existsSync9(row.path))
5228
+ if (!row || row.kind !== "worktree" || row.state === "removed" || !existsSync10(row.path))
5110
5229
  return null;
5111
5230
  return row;
5112
5231
  }
@@ -5440,15 +5559,15 @@ class QuestionQueue {
5440
5559
  // src/acp/fs.ts
5441
5560
  import { randomUUID } from "crypto";
5442
5561
  import {
5443
- existsSync as existsSync10,
5444
- mkdirSync as mkdirSync5,
5562
+ existsSync as existsSync11,
5563
+ mkdirSync as mkdirSync6,
5445
5564
  readFileSync as readFileSync6,
5446
5565
  realpathSync,
5447
- renameSync as renameSync3,
5448
- rmSync as rmSync3,
5566
+ renameSync as renameSync4,
5567
+ rmSync as rmSync4,
5449
5568
  writeFileSync as writeFileSync4
5450
5569
  } from "fs";
5451
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
5570
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
5452
5571
 
5453
5572
  class OutsideWorktree extends Error {
5454
5573
  path;
@@ -5486,11 +5605,11 @@ class ConfinedFs {
5486
5605
  return real;
5487
5606
  }
5488
5607
  resolveParent(absolute) {
5489
- let dir = dirname6(absolute);
5608
+ let dir = dirname7(absolute);
5490
5609
  const missing = [];
5491
- while (!existsSync10(dir) && dirname6(dir) !== dir) {
5610
+ while (!existsSync11(dir) && dirname7(dir) !== dir) {
5492
5611
  missing.unshift(basename2(dir));
5493
- dir = dirname6(dir);
5612
+ dir = dirname7(dir);
5494
5613
  }
5495
5614
  let real;
5496
5615
  try {
@@ -5524,20 +5643,20 @@ class ConfinedFs {
5524
5643
  write(path, content) {
5525
5644
  const absolute = this.absolute(path);
5526
5645
  let target;
5527
- if (existsSync10(absolute)) {
5646
+ if (existsSync11(absolute)) {
5528
5647
  target = realpathSync(absolute);
5529
5648
  if (!this.inside(target))
5530
5649
  throw new OutsideWorktree(path);
5531
5650
  } else {
5532
5651
  target = join7(this.resolveParent(absolute), basename2(absolute));
5533
5652
  }
5534
- mkdirSync5(dirname6(target), { recursive: true });
5535
- const temp = join7(dirname6(target), `.${basename2(target)}.${randomUUID()}.tmp`);
5653
+ mkdirSync6(dirname7(target), { recursive: true });
5654
+ const temp = join7(dirname7(target), `.${basename2(target)}.${randomUUID()}.tmp`);
5536
5655
  try {
5537
5656
  writeFileSync4(temp, content);
5538
- renameSync3(temp, target);
5657
+ renameSync4(temp, target);
5539
5658
  } catch (error) {
5540
- rmSync3(temp, { force: true });
5659
+ rmSync4(temp, { force: true });
5541
5660
  throw error;
5542
5661
  }
5543
5662
  }
@@ -6490,7 +6609,7 @@ class Turn {
6490
6609
  this.cwd = this.workspace.cwd;
6491
6610
  this.branch = this.workspace.branch;
6492
6611
  } else {
6493
- mkdirSync6(this.cwd, { recursive: true, mode: 448 });
6612
+ mkdirSync7(this.cwd, { recursive: true, mode: 448 });
6494
6613
  }
6495
6614
  if (this.endReason)
6496
6615
  throw new TurnFailure("stopped");
@@ -6818,6 +6937,13 @@ class Turn {
6818
6937
  "hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
6819
6938
  "hypotenuse.acp.resumed": this.resumed
6820
6939
  }, promptText);
6940
+ if (ctx.log.enabled("debug")) {
6941
+ ctx.log.debug(`prompting ${this.agentId}: ${preview(promptText, ctx.log.level)}`, {
6942
+ session: ctx.assignment.agentSessionId,
6943
+ workItem: ctx.assignment.workItemId,
6944
+ bytes: Buffer.byteLength(promptText)
6945
+ });
6946
+ }
6821
6947
  const request = conn.prompt(sessionId, blocks);
6822
6948
  this.promptSettled = request.then(() => {}, () => {});
6823
6949
  const answer = await this.guard(request, 0, "").catch((error) => {
@@ -7028,7 +7154,7 @@ class Turn {
7028
7154
 
7029
7155
  // src/runner/machine.ts
7030
7156
  import { createHash } from "crypto";
7031
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
7157
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
7032
7158
  import { hostname as hostname2 } from "os";
7033
7159
  var MAX_NAME = 64;
7034
7160
  function defaultRunnerName(host = hostname2()) {
@@ -7041,7 +7167,7 @@ function rawMachineId(env = process.env) {
7041
7167
  try {
7042
7168
  if (process.platform === "linux") {
7043
7169
  for (const path of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
7044
- if (existsSync11(path)) {
7170
+ if (existsSync12(path)) {
7045
7171
  const id = readFileSync8(path, "utf8").trim();
7046
7172
  if (id)
7047
7173
  return id;
@@ -7098,8 +7224,8 @@ function registrationHeartbeat() {
7098
7224
  }
7099
7225
 
7100
7226
  // src/daemon/control-socket.ts
7101
- import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "fs";
7102
- import { dirname as dirname7 } from "path";
7227
+ import { chmodSync as chmodSync4, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
7228
+ import { dirname as dirname8 } from "path";
7103
7229
  var METHOD_NOT_FOUND = -32601;
7104
7230
  var INVALID_PARAMS = -32602;
7105
7231
  var INTERNAL_ERROR = -32603;
@@ -7124,9 +7250,9 @@ class ControlServer {
7124
7250
  this.onError = onError;
7125
7251
  }
7126
7252
  listen() {
7127
- mkdirSync7(dirname7(this.path), { recursive: true, mode: 448 });
7128
- if (existsSync12(this.path))
7129
- rmSync4(this.path, { force: true });
7253
+ mkdirSync8(dirname8(this.path), { recursive: true, mode: 448 });
7254
+ if (existsSync13(this.path))
7255
+ rmSync5(this.path, { force: true });
7130
7256
  const previous = process.umask(127);
7131
7257
  try {
7132
7258
  this.listener = Bun.listen({
@@ -7142,12 +7268,12 @@ class ControlServer {
7142
7268
  } finally {
7143
7269
  process.umask(previous);
7144
7270
  }
7145
- chmodSync3(this.path, 384);
7271
+ chmodSync4(this.path, 384);
7146
7272
  }
7147
7273
  close() {
7148
7274
  this.listener?.stop(true);
7149
7275
  this.listener = null;
7150
- rmSync4(this.path, { force: true });
7276
+ rmSync5(this.path, { force: true });
7151
7277
  }
7152
7278
  onData(socket, data) {
7153
7279
  socket.data.buffer += data.toString("utf8");
@@ -7286,90 +7412,6 @@ class DaemonApi {
7286
7412
  }
7287
7413
  }
7288
7414
 
7289
- // src/daemon/log.ts
7290
- import {
7291
- appendFileSync as appendFileSync2,
7292
- chmodSync as chmodSync4,
7293
- existsSync as existsSync13,
7294
- mkdirSync as mkdirSync8,
7295
- renameSync as renameSync4,
7296
- rmSync as rmSync5,
7297
- statSync as statSync4
7298
- } from "fs";
7299
- import { dirname as dirname8 } from "path";
7300
- var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
7301
- var LOG_KEEP = 5;
7302
- var RANK = {
7303
- error: 0,
7304
- warn: 1,
7305
- info: 2,
7306
- debug: 3,
7307
- trace: 4
7308
- };
7309
-
7310
- class DaemonLog {
7311
- file;
7312
- level;
7313
- echo;
7314
- bytes = 0;
7315
- constructor(file, level, echo) {
7316
- this.file = file;
7317
- this.level = level;
7318
- this.echo = echo;
7319
- if (file) {
7320
- mkdirSync8(dirname8(file), { recursive: true, mode: 448 });
7321
- this.bytes = existsSync13(file) ? statSync4(file).size : 0;
7322
- }
7323
- }
7324
- enabled(level) {
7325
- return RANK[level] <= RANK[this.level];
7326
- }
7327
- write(level, message, fields = {}) {
7328
- if (!this.enabled(level))
7329
- return;
7330
- const extra = Object.entries(fields).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${typeof v === "string" && /\s/.test(v) ? JSON.stringify(v) : v}`).join(" ");
7331
- const line = `${new Date().toISOString()} ${level.padEnd(5)} ${message}${extra ? ` ${extra}` : ""}
7332
- `;
7333
- if (this.echo)
7334
- process.stderr.write(line);
7335
- if (!this.file)
7336
- return;
7337
- try {
7338
- if (this.bytes + line.length > LOG_ROTATE_BYTES)
7339
- this.rotate();
7340
- const fresh = !existsSync13(this.file);
7341
- appendFileSync2(this.file, line, { mode: 384 });
7342
- if (fresh)
7343
- chmodSync4(this.file, 384);
7344
- this.bytes += Buffer.byteLength(line);
7345
- } catch {}
7346
- }
7347
- rotate() {
7348
- if (!this.file)
7349
- return;
7350
- rmSync5(`${this.file}.${LOG_KEEP}`, { force: true });
7351
- for (let i = LOG_KEEP - 1;i >= 1; i--) {
7352
- if (existsSync13(`${this.file}.${i}`))
7353
- renameSync4(`${this.file}.${i}`, `${this.file}.${i + 1}`);
7354
- }
7355
- if (existsSync13(this.file))
7356
- renameSync4(this.file, `${this.file}.1`);
7357
- this.bytes = 0;
7358
- }
7359
- error(message, fields) {
7360
- this.write("error", message, fields);
7361
- }
7362
- warn(message, fields) {
7363
- this.write("warn", message, fields);
7364
- }
7365
- info(message, fields) {
7366
- this.write("info", message, fields);
7367
- }
7368
- debug(message, fields) {
7369
- this.write("debug", message, fields);
7370
- }
7371
- }
7372
-
7373
7415
  // src/daemon/pidfile.ts
7374
7416
  import {
7375
7417
  closeSync as closeSync2,
@@ -8942,6 +8984,46 @@ class Backoff {
8942
8984
  // src/daemon/event-sink.ts
8943
8985
  var BATCH_EVENTS = 50;
8944
8986
  var BATCH_BYTES = 64 * 1024;
8987
+ function describeEvent(event) {
8988
+ let text;
8989
+ switch (event.type) {
8990
+ case "thought":
8991
+ case "response":
8992
+ case "error":
8993
+ text = event.body;
8994
+ break;
8995
+ case "elicitation":
8996
+ text = event.signal ? `[${event.signal}] ${event.body}` : event.body;
8997
+ break;
8998
+ case "action":
8999
+ text = `${event.action} ${event.parameter}${event.result ? ` \u2192 ${event.result}` : ""}`;
9000
+ break;
9001
+ case "plan": {
9002
+ const done = event.entries.filter((e) => e.status === "completed").length;
9003
+ const now = event.entries.find((e) => e.status === "in_progress");
9004
+ text = `${done}/${event.entries.length} done${now ? `; now: ${now.content}` : ""}`;
9005
+ break;
9006
+ }
9007
+ case "summary":
9008
+ text = event.title;
9009
+ break;
9010
+ case "external_url":
9011
+ text = `${event.label} ${event.url}`;
9012
+ break;
9013
+ case "usage":
9014
+ text = [
9015
+ event.inputTokens !== undefined && `in=${event.inputTokens}`,
9016
+ event.outputTokens !== undefined && `out=${event.outputTokens}`,
9017
+ event.costUsd !== undefined && `cost=$${event.costUsd}`,
9018
+ event.contextUsed !== undefined && `context=${event.contextUsed}${event.contextSize ? `/${event.contextSize}` : ""}`
9019
+ ].filter(Boolean).join(" ");
9020
+ break;
9021
+ case "status":
9022
+ text = event.state;
9023
+ break;
9024
+ }
9025
+ return text;
9026
+ }
8945
9027
 
8946
9028
  class EventSink {
8947
9029
  state;
@@ -8971,13 +9053,14 @@ class EventSink {
8971
9053
  ephemeral,
8972
9054
  payload: { ...event, eventId, at: Date.now() }
8973
9055
  });
8974
- this.log.debug("event queued", {
8975
- session: target.agentSessionId,
8976
- workItem: target.workItemId,
8977
- type: event.type,
8978
- seq,
8979
- ephemeral
8980
- });
9056
+ if (this.log.enabled("debug")) {
9057
+ this.log.debug(`agent ${event.type}: ${preview(describeEvent(event), this.log.level)}`, {
9058
+ session: target.agentSessionId,
9059
+ workItem: target.workItemId,
9060
+ seq,
9061
+ ephemeral
9062
+ });
9063
+ }
8981
9064
  this.kick();
8982
9065
  return { eventId, seq };
8983
9066
  }
@@ -9872,6 +9955,14 @@ class Supervisor {
9872
9955
  });
9873
9956
  if (taken)
9874
9957
  slot.consumed.push(reply.activityId);
9958
+ const { log } = this.options;
9959
+ if (log.enabled("debug")) {
9960
+ log.debug(`reply: ${preview(reply.body, log.level)}`, {
9961
+ session: slot.agentSessionId,
9962
+ workItem: slot.workItemId,
9963
+ activity: reply.activityId
9964
+ });
9965
+ }
9875
9966
  this.options.log.info(taken ? "a reply answered the turn's question" : "a reply did not answer the turn's question; it becomes the next prompt", { session: slot.agentSessionId, workItem: slot.workItemId });
9876
9967
  }
9877
9968
  agentFor(role) {
@@ -10681,7 +10772,7 @@ function notRunning(ctx, error) {
10681
10772
  }
10682
10773
  throw error;
10683
10774
  }
10684
- async function startBackground(ctx) {
10775
+ async function startBackground(ctx, logLevel) {
10685
10776
  const location = locateDaemon(ctx);
10686
10777
  const lock = await inspectLock(location.pidfile);
10687
10778
  if (lock.kind === "running" || lock.kind === "starting" || lock.kind === "hung") {
@@ -10696,7 +10787,8 @@ async function startBackground(ctx) {
10696
10787
  ...o.project ? ["--project", o.project] : [],
10697
10788
  ...o.verbose ? ["--verbose"] : [],
10698
10789
  "daemon",
10699
- "start"
10790
+ "start",
10791
+ ...logLevel ? ["--log-level", logLevel] : []
10700
10792
  ]);
10701
10793
  const child = spawn3(command, [...prefix, ...args], {
10702
10794
  detached: true,
@@ -10792,7 +10884,7 @@ async function tailLog(file, options) {
10792
10884
  const all = readFileSync11(file, "utf8").split(`
10793
10885
  `).filter(Boolean).filter(keep);
10794
10886
  for (const line of all.slice(-options.lines))
10795
- console.log(line);
10887
+ console.log(paintLogLine(line));
10796
10888
  }
10797
10889
  if (!options.follow)
10798
10890
  return;
@@ -10818,12 +10910,15 @@ async function tailLog(file, options) {
10818
10910
  partial = lines.pop() ?? "";
10819
10911
  for (const line of lines)
10820
10912
  if (line && keep(line))
10821
- console.log(line);
10913
+ console.log(paintLogLine(line));
10822
10914
  }
10823
10915
  }
10916
+ function logLevelOption() {
10917
+ return new Option("--log-level <level>", "the log level, over -v, HYPOTENUSE_LOG_LEVEL, and the config file").choices(LOG_LEVELS);
10918
+ }
10824
10919
  function registerDaemonCommands(program) {
10825
10920
  const daemon = program.command("daemon").description("Run this machine as a runner");
10826
- daemon.command("start").description("Start the daemon (in the background unless --foreground)").option("--foreground", "run in this process; what a service manager runs").addOption(new Option("--detached-child").hideHelp()).action(async function() {
10921
+ daemon.command("start").description("Start the daemon (in the background unless --foreground)").option("--foreground", "run in this process; what a service manager runs").addOption(logLevelOption()).addOption(new Option("--detached-child").hideHelp()).action(async function() {
10827
10922
  const opts = this.opts();
10828
10923
  if (opts.foreground) {
10829
10924
  const globals = this.optsWithGlobals();
@@ -10833,12 +10928,13 @@ function registerDaemonCommands(program) {
10833
10928
  url: globals.url,
10834
10929
  project: globals.project,
10835
10930
  verbose: globals.verbose,
10931
+ logLevel: opts.logLevel,
10836
10932
  detachedChild: Boolean(opts.detachedChild)
10837
10933
  });
10838
10934
  return;
10839
10935
  }
10840
10936
  await runCommand(this, async (ctx) => {
10841
- const pid = await startBackground(ctx);
10937
+ const pid = await startBackground(ctx, opts.logLevel);
10842
10938
  const { log } = locateDaemon(ctx);
10843
10939
  emit(ctx, { started: true, pid }, () => `Started the daemon (pid ${pid}). Logs: ${tildify(log, ctx.env)}`);
10844
10940
  });
@@ -10858,7 +10954,7 @@ function registerDaemonCommands(program) {
10858
10954
  }
10859
10955
  });
10860
10956
  });
10861
- daemon.command("restart").description("Stop the daemon (draining) and start it again").option("--timeout <duration>", "how long running turns may take before they are stopped", "10m").option("--cancel", "stop running turns at once").action(async function() {
10957
+ daemon.command("restart").description("Stop the daemon (draining) and start it again").option("--timeout <duration>", "how long running turns may take before they are stopped", "10m").option("--cancel", "stop running turns at once").addOption(logLevelOption()).action(async function() {
10862
10958
  await runCommand(this, async (ctx) => {
10863
10959
  const opts = this.opts();
10864
10960
  try {
@@ -10871,7 +10967,7 @@ function registerDaemonCommands(program) {
10871
10967
  if (!(error instanceof DaemonNotRunning))
10872
10968
  throw error;
10873
10969
  }
10874
- const pid = await startBackground(ctx);
10970
+ const pid = await startBackground(ctx, opts.logLevel);
10875
10971
  emit(ctx, { restarted: true, pid }, () => `Restarted the daemon (pid ${pid}).`);
10876
10972
  });
10877
10973
  });
@@ -12026,7 +12122,7 @@ function turnPlace(event) {
12026
12122
  }
12027
12123
  return t.runner ? `${t.status} on ${t.runner.name}` : t.status;
12028
12124
  }
12029
- function describeEvent(event, payload) {
12125
+ function describeEvent2(event, payload) {
12030
12126
  const lines = [
12031
12127
  `${event.eventType} ${event.action}${event.issueIdentifier ? ` ${event.issueIdentifier}` : ""}`,
12032
12128
  `outcome ${event.outcome}`,
@@ -12093,7 +12189,7 @@ function registerEventCommands(program) {
12093
12189
  await runCommand(this, async (ctx) => {
12094
12190
  const projectId = await ctx.projectId();
12095
12191
  const result = await ctx.signedInClient().request("GET", `/trigger-event/${id}`, { query: { projectId } });
12096
- emit(ctx, result, () => describeEvent(result.triggerEvent, result.payload));
12192
+ emit(ctx, result, () => describeEvent2(result.triggerEvent, result.payload));
12097
12193
  });
12098
12194
  });
12099
12195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadeai/hypotenuse",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Hypotenuse — run coding agents on your machine for Linear agent sessions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -41,7 +41,7 @@ server. It calls the same HTTP actions the dashboard does, as the person who sig
41
41
  | `hypotenuse telemetry exporter list\|view\|add\|edit\|remove\|test` | Where the selected project's OpenTelemetry goes: each exporter is an OTLP/HTTP collector that receives every turn as one trace, at a content level (`metadata`, `content`, or `full` — full sends prompts and source code). `list` and `view <id>` show endpoint, level, signals, sample share, header names (never values), and the last export or error (any member); `add --name <name> --endpoint <https-url> [--header KEY=VALUE...] [--content <level>] [--signals traces,metrics,logs] [--sample 0.5] [--attribute KEY=VALUE...] [--no-agent-native] [--disabled]`; `edit <id>` with the same flags plus `--clear-headers` and `--enable\|--disable` (headers are kept unless given); `remove <id>`; `test <id>` sends a probe and prints each signal's answer, exiting 1 on a failure. Changes and tests are admin-only; header values are never printed. |
42
42
  | `hypotenuse identity list\|link\|unlink` | Links between people in the project's Linear workspace and project members; routing prefers the runners of whoever started a session. `list [--user <id>]` shows each link and how it was made (e-mail match, Linear sign-in, admin); `link --user <id> --linear-user <id> [--name] [--email]` links one (admin); `unlink <id>` removes your own link, or anyone's as an admin. |
43
43
  | `hypotenuse config init\|validate\|schema\|path` | The daemon's TOML config (`-c <path>`, else `HYPOTENUSE_CONFIG`, else `~/.config/hypotenuse/config.toml`). `init [--repo <path>...] [--force]` writes a commented starter filled in from this machine; `validate [--online] [--offline]` checks shape, references, and this machine (git clones, prompt files, agent commands), and with `--online` the server, runner token, project, and team keys — exit 0 valid (warnings allowed), 1 errors, 2 unreadable; `--json` prints `{ valid, errors: [{ path, message, hint? }], warnings }`. `schema` prints the JSON Schema; `path` prints the config, state directory, and credentials file in use. |
44
- | `hypotenuse daemon start\|stop\|restart\|status\|logs\|reload` | The runner daemon. `start` backgrounds it and waits until it is ready (`--foreground` stays attached, as a service manager runs it); `stop [--timeout 10m] [--cancel]` lets running turns finish (or stops them) and exits, `--force` signals it without its control socket; `status` exits 3 when it is not running; `logs [-f] [-n 200] [--session <id>]` (`--session` shows that session's agent log: the agent's stderr and ACP trace); `reload` re-reads the config and says what needs a restart. One daemon per machine. |
44
+ | `hypotenuse daemon start\|stop\|restart\|status\|logs\|reload` | The runner daemon. `start` backgrounds it and waits until it is ready (`--foreground` stays attached, as a service manager runs it; `--log-level <level>` overrides `[log] level`, and `debug` logs every agent activity); `stop [--timeout 10m] [--cancel]` lets running turns finish (or stops them) and exits, `--force` signals it without its control socket; `status` exits 3 when it is not running; `logs [-f] [-n 200] [--session <id>]` (`--session` shows that session's agent log: the agent's stderr and ACP trace); `reload` re-reads the config and says what needs a restart. One daemon per machine. |
45
45
  | `hypotenuse agents list\|kill\|login` | The coding agents this machine runs. `list [--all]` shows the agent sessions the daemon is running (session, issue, role, agent, state, how long, pid, worktree); `kill <session-id\|ENG-123> [--reason]` stops that session's running turn, which says so in Linear (with the reason); both exit 3 when the daemon is not running. `login <agent> [--method <id>]` logs a coding agent in on this machine — a terminal login runs attached, an agent-run one over ACP — then confirms it can open a session (exit 1 if not). |
46
46
  | `hypotenuse worktrees list\|cleanup` | The git worktrees (and scratch directories) the daemon made for its sessions, one per session under `sessions.worktree_root`. `list` shows each one's repository, branch, issue, state, last use, size, and whether it is safe to remove now (not in use, no uncommitted changes, no unpushed commits, no `.hypotenuse-keep` file). `cleanup [--dry-run] [--all] [--older-than 7d] [--force]` removes what the cleanup policy allows and prints every kept one with its reason; `--all` ignores the policy, `--force` overrides every rule but "in use". Both work without a running daemon, from `state.db` and the disk. |
47
47
  | `hypotenuse service install\|uninstall\|status` | Run the daemon as a user service (launchd on macOS, systemd user unit on Linux) with the `PATH` of the installing shell; `install --print` shows the unit without installing it. Windows: prints the foreground command and the Task Scheduler recipe. |