@arcadeai/hypotenuse 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hypotenuse.js +240 -145
- package/package.json +1 -1
- package/skills/hypotenuse.md +1 -1
package/dist/hypotenuse.js
CHANGED
|
@@ -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.
|
|
346
|
+
version: "0.16.0",
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4486
|
-
|
|
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
|
|
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
|
|
4530
|
-
import { dirname as
|
|
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 (!
|
|
4534
|
-
const parent =
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
4775
|
+
rmSync3(file, { force: true });
|
|
4658
4776
|
}
|
|
4659
4777
|
|
|
4660
4778
|
// src/worktree/setup.ts
|
|
@@ -4823,7 +4941,7 @@ class WorktreeManager {
|
|
|
4823
4941
|
this.refuseIfShort(request);
|
|
4824
4942
|
await this.fetchBase(request, repository, baseBranch);
|
|
4825
4943
|
const path = this.worktreePath(request, repository);
|
|
4826
|
-
|
|
4944
|
+
mkdirSync5(join6(path, ".."), { recursive: true, mode: 448 });
|
|
4827
4945
|
await this.git(["worktree", "prune"], { cwd: repository.path });
|
|
4828
4946
|
await this.must(["worktree", "add", "--detach", path, `origin/${baseBranch}`], repository.path, `create a read-only checkout of ${baseBranch}`);
|
|
4829
4947
|
const created = this.state.insertWorktree({
|
|
@@ -4870,7 +4988,7 @@ class WorktreeManager {
|
|
|
4870
4988
|
async scratch(request) {
|
|
4871
4989
|
const id = request.assignment.agentSessionId;
|
|
4872
4990
|
const path = join6(request.config.sessions.worktree_root, SCRATCH, String(id));
|
|
4873
|
-
|
|
4991
|
+
mkdirSync5(path, { recursive: true, mode: 448 });
|
|
4874
4992
|
const known = this.state.worktreeAt(path);
|
|
4875
4993
|
let row;
|
|
4876
4994
|
if (known && known.state !== "removed") {
|
|
@@ -4950,7 +5068,7 @@ ${stderrTail(fetched)}
|
|
|
4950
5068
|
}
|
|
4951
5069
|
fetchAge(repoPath) {
|
|
4952
5070
|
try {
|
|
4953
|
-
const age = this.now() -
|
|
5071
|
+
const age = this.now() - statSync4(join6(repoPath, ".git", "FETCH_HEAD")).mtimeMs;
|
|
4954
5072
|
if (age < 60000)
|
|
4955
5073
|
return null;
|
|
4956
5074
|
const units = [
|
|
@@ -5007,7 +5125,7 @@ ${stderrTail(fetched)}
|
|
|
5007
5125
|
for (let n = 1;; n++) {
|
|
5008
5126
|
const path = join6(parent, suffixed(dir, n));
|
|
5009
5127
|
const row = this.state.worktreeAt(path);
|
|
5010
|
-
if (!
|
|
5128
|
+
if (!existsSync10(path) && (!row || row.state === "removed"))
|
|
5011
5129
|
return path;
|
|
5012
5130
|
}
|
|
5013
5131
|
}
|
|
@@ -5062,7 +5180,7 @@ ${stderrTail(fetched)}
|
|
|
5062
5180
|
const reachable = await this.fetchBase(request, repository, baseBranch);
|
|
5063
5181
|
const { branch, local, remote } = await this.chooseBranch(request, repository, repoConfig, reachable);
|
|
5064
5182
|
const path = this.worktreePath(request, repository);
|
|
5065
|
-
|
|
5183
|
+
mkdirSync5(join6(path, ".."), { recursive: true, mode: 448 });
|
|
5066
5184
|
await this.git(["worktree", "prune"], { cwd: repository.path });
|
|
5067
5185
|
let add;
|
|
5068
5186
|
if (local) {
|
|
@@ -5106,7 +5224,7 @@ ${stderrTail(fetched)}
|
|
|
5106
5224
|
reusable(agentSessionId) {
|
|
5107
5225
|
const session = this.state.session(agentSessionId);
|
|
5108
5226
|
const row = session?.worktreeId ? this.state.worktree(session.worktreeId) : null;
|
|
5109
|
-
if (!row || row.kind !== "worktree" || row.state === "removed" || !
|
|
5227
|
+
if (!row || row.kind !== "worktree" || row.state === "removed" || !existsSync10(row.path))
|
|
5110
5228
|
return null;
|
|
5111
5229
|
return row;
|
|
5112
5230
|
}
|
|
@@ -5440,15 +5558,15 @@ class QuestionQueue {
|
|
|
5440
5558
|
// src/acp/fs.ts
|
|
5441
5559
|
import { randomUUID } from "crypto";
|
|
5442
5560
|
import {
|
|
5443
|
-
existsSync as
|
|
5444
|
-
mkdirSync as
|
|
5561
|
+
existsSync as existsSync11,
|
|
5562
|
+
mkdirSync as mkdirSync6,
|
|
5445
5563
|
readFileSync as readFileSync6,
|
|
5446
5564
|
realpathSync,
|
|
5447
|
-
renameSync as
|
|
5448
|
-
rmSync as
|
|
5565
|
+
renameSync as renameSync4,
|
|
5566
|
+
rmSync as rmSync4,
|
|
5449
5567
|
writeFileSync as writeFileSync4
|
|
5450
5568
|
} from "fs";
|
|
5451
|
-
import { basename as basename2, dirname as
|
|
5569
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
|
|
5452
5570
|
|
|
5453
5571
|
class OutsideWorktree extends Error {
|
|
5454
5572
|
path;
|
|
@@ -5486,11 +5604,11 @@ class ConfinedFs {
|
|
|
5486
5604
|
return real;
|
|
5487
5605
|
}
|
|
5488
5606
|
resolveParent(absolute) {
|
|
5489
|
-
let dir =
|
|
5607
|
+
let dir = dirname7(absolute);
|
|
5490
5608
|
const missing = [];
|
|
5491
|
-
while (!
|
|
5609
|
+
while (!existsSync11(dir) && dirname7(dir) !== dir) {
|
|
5492
5610
|
missing.unshift(basename2(dir));
|
|
5493
|
-
dir =
|
|
5611
|
+
dir = dirname7(dir);
|
|
5494
5612
|
}
|
|
5495
5613
|
let real;
|
|
5496
5614
|
try {
|
|
@@ -5524,20 +5642,20 @@ class ConfinedFs {
|
|
|
5524
5642
|
write(path, content) {
|
|
5525
5643
|
const absolute = this.absolute(path);
|
|
5526
5644
|
let target;
|
|
5527
|
-
if (
|
|
5645
|
+
if (existsSync11(absolute)) {
|
|
5528
5646
|
target = realpathSync(absolute);
|
|
5529
5647
|
if (!this.inside(target))
|
|
5530
5648
|
throw new OutsideWorktree(path);
|
|
5531
5649
|
} else {
|
|
5532
5650
|
target = join7(this.resolveParent(absolute), basename2(absolute));
|
|
5533
5651
|
}
|
|
5534
|
-
|
|
5535
|
-
const temp = join7(
|
|
5652
|
+
mkdirSync6(dirname7(target), { recursive: true });
|
|
5653
|
+
const temp = join7(dirname7(target), `.${basename2(target)}.${randomUUID()}.tmp`);
|
|
5536
5654
|
try {
|
|
5537
5655
|
writeFileSync4(temp, content);
|
|
5538
|
-
|
|
5656
|
+
renameSync4(temp, target);
|
|
5539
5657
|
} catch (error) {
|
|
5540
|
-
|
|
5658
|
+
rmSync4(temp, { force: true });
|
|
5541
5659
|
throw error;
|
|
5542
5660
|
}
|
|
5543
5661
|
}
|
|
@@ -6490,7 +6608,7 @@ class Turn {
|
|
|
6490
6608
|
this.cwd = this.workspace.cwd;
|
|
6491
6609
|
this.branch = this.workspace.branch;
|
|
6492
6610
|
} else {
|
|
6493
|
-
|
|
6611
|
+
mkdirSync7(this.cwd, { recursive: true, mode: 448 });
|
|
6494
6612
|
}
|
|
6495
6613
|
if (this.endReason)
|
|
6496
6614
|
throw new TurnFailure("stopped");
|
|
@@ -6818,6 +6936,13 @@ class Turn {
|
|
|
6818
6936
|
"hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
|
|
6819
6937
|
"hypotenuse.acp.resumed": this.resumed
|
|
6820
6938
|
}, promptText);
|
|
6939
|
+
if (ctx.log.enabled("debug")) {
|
|
6940
|
+
ctx.log.debug(`prompting ${this.agentId}: ${preview(promptText, ctx.log.level)}`, {
|
|
6941
|
+
session: ctx.assignment.agentSessionId,
|
|
6942
|
+
workItem: ctx.assignment.workItemId,
|
|
6943
|
+
bytes: Buffer.byteLength(promptText)
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6821
6946
|
const request = conn.prompt(sessionId, blocks);
|
|
6822
6947
|
this.promptSettled = request.then(() => {}, () => {});
|
|
6823
6948
|
const answer = await this.guard(request, 0, "").catch((error) => {
|
|
@@ -7028,7 +7153,7 @@ class Turn {
|
|
|
7028
7153
|
|
|
7029
7154
|
// src/runner/machine.ts
|
|
7030
7155
|
import { createHash } from "crypto";
|
|
7031
|
-
import { existsSync as
|
|
7156
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
7032
7157
|
import { hostname as hostname2 } from "os";
|
|
7033
7158
|
var MAX_NAME = 64;
|
|
7034
7159
|
function defaultRunnerName(host = hostname2()) {
|
|
@@ -7041,7 +7166,7 @@ function rawMachineId(env = process.env) {
|
|
|
7041
7166
|
try {
|
|
7042
7167
|
if (process.platform === "linux") {
|
|
7043
7168
|
for (const path of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
7044
|
-
if (
|
|
7169
|
+
if (existsSync12(path)) {
|
|
7045
7170
|
const id = readFileSync8(path, "utf8").trim();
|
|
7046
7171
|
if (id)
|
|
7047
7172
|
return id;
|
|
@@ -7098,8 +7223,8 @@ function registrationHeartbeat() {
|
|
|
7098
7223
|
}
|
|
7099
7224
|
|
|
7100
7225
|
// src/daemon/control-socket.ts
|
|
7101
|
-
import { chmodSync as
|
|
7102
|
-
import { dirname as
|
|
7226
|
+
import { chmodSync as chmodSync4, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
|
|
7227
|
+
import { dirname as dirname8 } from "path";
|
|
7103
7228
|
var METHOD_NOT_FOUND = -32601;
|
|
7104
7229
|
var INVALID_PARAMS = -32602;
|
|
7105
7230
|
var INTERNAL_ERROR = -32603;
|
|
@@ -7124,9 +7249,9 @@ class ControlServer {
|
|
|
7124
7249
|
this.onError = onError;
|
|
7125
7250
|
}
|
|
7126
7251
|
listen() {
|
|
7127
|
-
|
|
7128
|
-
if (
|
|
7129
|
-
|
|
7252
|
+
mkdirSync8(dirname8(this.path), { recursive: true, mode: 448 });
|
|
7253
|
+
if (existsSync13(this.path))
|
|
7254
|
+
rmSync5(this.path, { force: true });
|
|
7130
7255
|
const previous = process.umask(127);
|
|
7131
7256
|
try {
|
|
7132
7257
|
this.listener = Bun.listen({
|
|
@@ -7142,12 +7267,12 @@ class ControlServer {
|
|
|
7142
7267
|
} finally {
|
|
7143
7268
|
process.umask(previous);
|
|
7144
7269
|
}
|
|
7145
|
-
|
|
7270
|
+
chmodSync4(this.path, 384);
|
|
7146
7271
|
}
|
|
7147
7272
|
close() {
|
|
7148
7273
|
this.listener?.stop(true);
|
|
7149
7274
|
this.listener = null;
|
|
7150
|
-
|
|
7275
|
+
rmSync5(this.path, { force: true });
|
|
7151
7276
|
}
|
|
7152
7277
|
onData(socket, data) {
|
|
7153
7278
|
socket.data.buffer += data.toString("utf8");
|
|
@@ -7286,90 +7411,6 @@ class DaemonApi {
|
|
|
7286
7411
|
}
|
|
7287
7412
|
}
|
|
7288
7413
|
|
|
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
7414
|
// src/daemon/pidfile.ts
|
|
7374
7415
|
import {
|
|
7375
7416
|
closeSync as closeSync2,
|
|
@@ -8942,6 +8983,46 @@ class Backoff {
|
|
|
8942
8983
|
// src/daemon/event-sink.ts
|
|
8943
8984
|
var BATCH_EVENTS = 50;
|
|
8944
8985
|
var BATCH_BYTES = 64 * 1024;
|
|
8986
|
+
function describeEvent(event) {
|
|
8987
|
+
let text;
|
|
8988
|
+
switch (event.type) {
|
|
8989
|
+
case "thought":
|
|
8990
|
+
case "response":
|
|
8991
|
+
case "error":
|
|
8992
|
+
text = event.body;
|
|
8993
|
+
break;
|
|
8994
|
+
case "elicitation":
|
|
8995
|
+
text = event.signal ? `[${event.signal}] ${event.body}` : event.body;
|
|
8996
|
+
break;
|
|
8997
|
+
case "action":
|
|
8998
|
+
text = `${event.action} ${event.parameter}${event.result ? ` \u2192 ${event.result}` : ""}`;
|
|
8999
|
+
break;
|
|
9000
|
+
case "plan": {
|
|
9001
|
+
const done = event.entries.filter((e) => e.status === "completed").length;
|
|
9002
|
+
const now = event.entries.find((e) => e.status === "in_progress");
|
|
9003
|
+
text = `${done}/${event.entries.length} done${now ? `; now: ${now.content}` : ""}`;
|
|
9004
|
+
break;
|
|
9005
|
+
}
|
|
9006
|
+
case "summary":
|
|
9007
|
+
text = event.title;
|
|
9008
|
+
break;
|
|
9009
|
+
case "external_url":
|
|
9010
|
+
text = `${event.label} ${event.url}`;
|
|
9011
|
+
break;
|
|
9012
|
+
case "usage":
|
|
9013
|
+
text = [
|
|
9014
|
+
event.inputTokens !== undefined && `in=${event.inputTokens}`,
|
|
9015
|
+
event.outputTokens !== undefined && `out=${event.outputTokens}`,
|
|
9016
|
+
event.costUsd !== undefined && `cost=$${event.costUsd}`,
|
|
9017
|
+
event.contextUsed !== undefined && `context=${event.contextUsed}${event.contextSize ? `/${event.contextSize}` : ""}`
|
|
9018
|
+
].filter(Boolean).join(" ");
|
|
9019
|
+
break;
|
|
9020
|
+
case "status":
|
|
9021
|
+
text = event.state;
|
|
9022
|
+
break;
|
|
9023
|
+
}
|
|
9024
|
+
return text;
|
|
9025
|
+
}
|
|
8945
9026
|
|
|
8946
9027
|
class EventSink {
|
|
8947
9028
|
state;
|
|
@@ -8971,13 +9052,14 @@ class EventSink {
|
|
|
8971
9052
|
ephemeral,
|
|
8972
9053
|
payload: { ...event, eventId, at: Date.now() }
|
|
8973
9054
|
});
|
|
8974
|
-
this.log.
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
9055
|
+
if (this.log.enabled("debug")) {
|
|
9056
|
+
this.log.debug(`agent ${event.type}: ${preview(describeEvent(event), this.log.level)}`, {
|
|
9057
|
+
session: target.agentSessionId,
|
|
9058
|
+
workItem: target.workItemId,
|
|
9059
|
+
seq,
|
|
9060
|
+
ephemeral
|
|
9061
|
+
});
|
|
9062
|
+
}
|
|
8981
9063
|
this.kick();
|
|
8982
9064
|
return { eventId, seq };
|
|
8983
9065
|
}
|
|
@@ -9872,6 +9954,14 @@ class Supervisor {
|
|
|
9872
9954
|
});
|
|
9873
9955
|
if (taken)
|
|
9874
9956
|
slot.consumed.push(reply.activityId);
|
|
9957
|
+
const { log } = this.options;
|
|
9958
|
+
if (log.enabled("debug")) {
|
|
9959
|
+
log.debug(`reply: ${preview(reply.body, log.level)}`, {
|
|
9960
|
+
session: slot.agentSessionId,
|
|
9961
|
+
workItem: slot.workItemId,
|
|
9962
|
+
activity: reply.activityId
|
|
9963
|
+
});
|
|
9964
|
+
}
|
|
9875
9965
|
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
9966
|
}
|
|
9877
9967
|
agentFor(role) {
|
|
@@ -10681,7 +10771,7 @@ function notRunning(ctx, error) {
|
|
|
10681
10771
|
}
|
|
10682
10772
|
throw error;
|
|
10683
10773
|
}
|
|
10684
|
-
async function startBackground(ctx) {
|
|
10774
|
+
async function startBackground(ctx, logLevel) {
|
|
10685
10775
|
const location = locateDaemon(ctx);
|
|
10686
10776
|
const lock = await inspectLock(location.pidfile);
|
|
10687
10777
|
if (lock.kind === "running" || lock.kind === "starting" || lock.kind === "hung") {
|
|
@@ -10696,7 +10786,8 @@ async function startBackground(ctx) {
|
|
|
10696
10786
|
...o.project ? ["--project", o.project] : [],
|
|
10697
10787
|
...o.verbose ? ["--verbose"] : [],
|
|
10698
10788
|
"daemon",
|
|
10699
|
-
"start"
|
|
10789
|
+
"start",
|
|
10790
|
+
...logLevel ? ["--log-level", logLevel] : []
|
|
10700
10791
|
]);
|
|
10701
10792
|
const child = spawn3(command, [...prefix, ...args], {
|
|
10702
10793
|
detached: true,
|
|
@@ -10792,7 +10883,7 @@ async function tailLog(file, options) {
|
|
|
10792
10883
|
const all = readFileSync11(file, "utf8").split(`
|
|
10793
10884
|
`).filter(Boolean).filter(keep);
|
|
10794
10885
|
for (const line of all.slice(-options.lines))
|
|
10795
|
-
console.log(line);
|
|
10886
|
+
console.log(paintLogLine(line));
|
|
10796
10887
|
}
|
|
10797
10888
|
if (!options.follow)
|
|
10798
10889
|
return;
|
|
@@ -10818,12 +10909,15 @@ async function tailLog(file, options) {
|
|
|
10818
10909
|
partial = lines.pop() ?? "";
|
|
10819
10910
|
for (const line of lines)
|
|
10820
10911
|
if (line && keep(line))
|
|
10821
|
-
console.log(line);
|
|
10912
|
+
console.log(paintLogLine(line));
|
|
10822
10913
|
}
|
|
10823
10914
|
}
|
|
10915
|
+
function logLevelOption() {
|
|
10916
|
+
return new Option("--log-level <level>", "the log level, over -v, HYPOTENUSE_LOG_LEVEL, and the config file").choices(LOG_LEVELS);
|
|
10917
|
+
}
|
|
10824
10918
|
function registerDaemonCommands(program) {
|
|
10825
10919
|
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() {
|
|
10920
|
+
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
10921
|
const opts = this.opts();
|
|
10828
10922
|
if (opts.foreground) {
|
|
10829
10923
|
const globals = this.optsWithGlobals();
|
|
@@ -10833,12 +10927,13 @@ function registerDaemonCommands(program) {
|
|
|
10833
10927
|
url: globals.url,
|
|
10834
10928
|
project: globals.project,
|
|
10835
10929
|
verbose: globals.verbose,
|
|
10930
|
+
logLevel: opts.logLevel,
|
|
10836
10931
|
detachedChild: Boolean(opts.detachedChild)
|
|
10837
10932
|
});
|
|
10838
10933
|
return;
|
|
10839
10934
|
}
|
|
10840
10935
|
await runCommand(this, async (ctx) => {
|
|
10841
|
-
const pid = await startBackground(ctx);
|
|
10936
|
+
const pid = await startBackground(ctx, opts.logLevel);
|
|
10842
10937
|
const { log } = locateDaemon(ctx);
|
|
10843
10938
|
emit(ctx, { started: true, pid }, () => `Started the daemon (pid ${pid}). Logs: ${tildify(log, ctx.env)}`);
|
|
10844
10939
|
});
|
|
@@ -10858,7 +10953,7 @@ function registerDaemonCommands(program) {
|
|
|
10858
10953
|
}
|
|
10859
10954
|
});
|
|
10860
10955
|
});
|
|
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() {
|
|
10956
|
+
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
10957
|
await runCommand(this, async (ctx) => {
|
|
10863
10958
|
const opts = this.opts();
|
|
10864
10959
|
try {
|
|
@@ -10871,7 +10966,7 @@ function registerDaemonCommands(program) {
|
|
|
10871
10966
|
if (!(error instanceof DaemonNotRunning))
|
|
10872
10967
|
throw error;
|
|
10873
10968
|
}
|
|
10874
|
-
const pid = await startBackground(ctx);
|
|
10969
|
+
const pid = await startBackground(ctx, opts.logLevel);
|
|
10875
10970
|
emit(ctx, { restarted: true, pid }, () => `Restarted the daemon (pid ${pid}).`);
|
|
10876
10971
|
});
|
|
10877
10972
|
});
|
|
@@ -12026,7 +12121,7 @@ function turnPlace(event) {
|
|
|
12026
12121
|
}
|
|
12027
12122
|
return t.runner ? `${t.status} on ${t.runner.name}` : t.status;
|
|
12028
12123
|
}
|
|
12029
|
-
function
|
|
12124
|
+
function describeEvent2(event, payload) {
|
|
12030
12125
|
const lines = [
|
|
12031
12126
|
`${event.eventType} ${event.action}${event.issueIdentifier ? ` ${event.issueIdentifier}` : ""}`,
|
|
12032
12127
|
`outcome ${event.outcome}`,
|
|
@@ -12093,7 +12188,7 @@ function registerEventCommands(program) {
|
|
|
12093
12188
|
await runCommand(this, async (ctx) => {
|
|
12094
12189
|
const projectId = await ctx.projectId();
|
|
12095
12190
|
const result = await ctx.signedInClient().request("GET", `/trigger-event/${id}`, { query: { projectId } });
|
|
12096
|
-
emit(ctx, result, () =>
|
|
12191
|
+
emit(ctx, result, () => describeEvent2(result.triggerEvent, result.payload));
|
|
12097
12192
|
});
|
|
12098
12193
|
});
|
|
12099
12194
|
}
|
package/package.json
CHANGED
package/skills/hypotenuse.md
CHANGED
|
@@ -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. |
|