@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.
- package/dist/hypotenuse.js +242 -146
- 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.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
|
|
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
|
|
@@ -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",
|
|
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
|
-
|
|
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
|
-
|
|
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() -
|
|
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 (!
|
|
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
|
-
|
|
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" || !
|
|
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
|
|
5444
|
-
mkdirSync as
|
|
5562
|
+
existsSync as existsSync11,
|
|
5563
|
+
mkdirSync as mkdirSync6,
|
|
5445
5564
|
readFileSync as readFileSync6,
|
|
5446
5565
|
realpathSync,
|
|
5447
|
-
renameSync as
|
|
5448
|
-
rmSync as
|
|
5566
|
+
renameSync as renameSync4,
|
|
5567
|
+
rmSync as rmSync4,
|
|
5449
5568
|
writeFileSync as writeFileSync4
|
|
5450
5569
|
} from "fs";
|
|
5451
|
-
import { basename as basename2, dirname as
|
|
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 =
|
|
5608
|
+
let dir = dirname7(absolute);
|
|
5490
5609
|
const missing = [];
|
|
5491
|
-
while (!
|
|
5610
|
+
while (!existsSync11(dir) && dirname7(dir) !== dir) {
|
|
5492
5611
|
missing.unshift(basename2(dir));
|
|
5493
|
-
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 (
|
|
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
|
-
|
|
5535
|
-
const temp = join7(
|
|
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
|
-
|
|
5657
|
+
renameSync4(temp, target);
|
|
5539
5658
|
} catch (error) {
|
|
5540
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
7102
|
-
import { dirname as
|
|
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
|
-
|
|
7128
|
-
if (
|
|
7129
|
-
|
|
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
|
-
|
|
7271
|
+
chmodSync4(this.path, 384);
|
|
7146
7272
|
}
|
|
7147
7273
|
close() {
|
|
7148
7274
|
this.listener?.stop(true);
|
|
7149
7275
|
this.listener = null;
|
|
7150
|
-
|
|
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.
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
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
|
|
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, () =>
|
|
12192
|
+
emit(ctx, result, () => describeEvent2(result.triggerEvent, result.payload));
|
|
12097
12193
|
});
|
|
12098
12194
|
});
|
|
12099
12195
|
}
|
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. |
|