@arcadeai/hypotenuse 0.17.0 → 0.20.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 +538 -319
- 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.20.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",
|
|
@@ -752,6 +752,173 @@ function isPreset(name) {
|
|
|
752
752
|
|
|
753
753
|
// src/config/resolve.ts
|
|
754
754
|
import { basename } from "path";
|
|
755
|
+
function agentById(config, id) {
|
|
756
|
+
const table = config.agents[id];
|
|
757
|
+
if (table) {
|
|
758
|
+
const base = table.preset ? [...AGENT_PRESETS[table.preset].command] : table.command ?? [];
|
|
759
|
+
return {
|
|
760
|
+
id,
|
|
761
|
+
preset: table.preset ?? null,
|
|
762
|
+
command: [...base, ...table.args],
|
|
763
|
+
env: table.env,
|
|
764
|
+
maxParallel: table.max_parallel
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (isPreset(id)) {
|
|
768
|
+
return {
|
|
769
|
+
id,
|
|
770
|
+
preset: id,
|
|
771
|
+
command: [...AGENT_PRESETS[id].command],
|
|
772
|
+
env: {},
|
|
773
|
+
maxParallel: undefined
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
function playedRoles(config) {
|
|
779
|
+
return config.runner.roles.flatMap((role) => {
|
|
780
|
+
const table = config.roles[role];
|
|
781
|
+
return table ? [[role, table]] : [];
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function resolveAgents(config) {
|
|
785
|
+
const byId = new Map;
|
|
786
|
+
for (const id of Object.keys(config.agents)) {
|
|
787
|
+
const agent = agentById(config, id);
|
|
788
|
+
if (agent)
|
|
789
|
+
byId.set(id, { ...agent, roles: [] });
|
|
790
|
+
}
|
|
791
|
+
for (const [role, table] of playedRoles(config)) {
|
|
792
|
+
const known = byId.get(table.agent);
|
|
793
|
+
if (known) {
|
|
794
|
+
if (!known.roles.includes(role))
|
|
795
|
+
known.roles.push(role);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const agent = agentById(config, table.agent);
|
|
799
|
+
if (agent)
|
|
800
|
+
byId.set(table.agent, { ...agent, roles: [role] });
|
|
801
|
+
}
|
|
802
|
+
return [...byId.values()];
|
|
803
|
+
}
|
|
804
|
+
function repositoryName(repo, origin) {
|
|
805
|
+
if (repo.name)
|
|
806
|
+
return repo.name;
|
|
807
|
+
const source = origin ?? repo.path;
|
|
808
|
+
return basename(source.replace(/\/+$/, "")) || "repository";
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/acp/presets.ts
|
|
812
|
+
var SESSION_VARIABLES = new Set([
|
|
813
|
+
"HYPOTENUSE_SESSION_ID",
|
|
814
|
+
"HYPOTENUSE_LINEAR_SESSION_ID",
|
|
815
|
+
"HYPOTENUSE_ISSUE",
|
|
816
|
+
"HYPOTENUSE_ISSUE_URL",
|
|
817
|
+
"HYPOTENUSE_BRANCH",
|
|
818
|
+
"HYPOTENUSE_BASE_BRANCH",
|
|
819
|
+
"HYPOTENUSE_ROLE",
|
|
820
|
+
"HYPOTENUSE_SESSION_URL"
|
|
821
|
+
]);
|
|
822
|
+
function expandVariables(value, env) {
|
|
823
|
+
return value.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_, braced, bare) => env[braced ?? bare] ?? "");
|
|
824
|
+
}
|
|
825
|
+
function sessionVariables(assignment, branch) {
|
|
826
|
+
return {
|
|
827
|
+
HYPOTENUSE_SESSION_ID: String(assignment.agentSessionId),
|
|
828
|
+
HYPOTENUSE_LINEAR_SESSION_ID: assignment.externalSessionId,
|
|
829
|
+
HYPOTENUSE_ISSUE: assignment.issue.identifier ?? "",
|
|
830
|
+
HYPOTENUSE_ISSUE_URL: assignment.issue.url ?? "",
|
|
831
|
+
HYPOTENUSE_BRANCH: branch ?? "",
|
|
832
|
+
HYPOTENUSE_BASE_BRANCH: assignment.repository?.baseBranch ?? "",
|
|
833
|
+
HYPOTENUSE_ROLE: assignment.role,
|
|
834
|
+
HYPOTENUSE_SESSION_URL: assignment.dashboardUrl,
|
|
835
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function sessionEnvironment(daemonEnv, extra) {
|
|
839
|
+
const env = {};
|
|
840
|
+
for (const [key, value] of Object.entries(daemonEnv)) {
|
|
841
|
+
if (value === undefined)
|
|
842
|
+
continue;
|
|
843
|
+
if (key.startsWith("HYPOTENUSE_") && !SESSION_VARIABLES.has(key))
|
|
844
|
+
continue;
|
|
845
|
+
env[key] = value;
|
|
846
|
+
}
|
|
847
|
+
return Object.assign(env, extra);
|
|
848
|
+
}
|
|
849
|
+
function resolveAgentLaunch(config, agentId, daemonEnv, extra = {}) {
|
|
850
|
+
const agent = resolveAgents(config).find((a) => a.id === agentId);
|
|
851
|
+
if (!agent || !agent.command.length)
|
|
852
|
+
return null;
|
|
853
|
+
const table = config.agents[agentId];
|
|
854
|
+
const env = sessionEnvironment(daemonEnv, {});
|
|
855
|
+
const preset = agent.preset ? AGENT_PRESETS[agent.preset] : null;
|
|
856
|
+
if (preset && "env" in preset && preset.env)
|
|
857
|
+
Object.assign(env, preset.env);
|
|
858
|
+
if (agent.command[0] === "npx")
|
|
859
|
+
env.npm_config_yes = "true";
|
|
860
|
+
for (const [key, value] of Object.entries(agent.env))
|
|
861
|
+
env[key] = expandVariables(value, daemonEnv);
|
|
862
|
+
Object.assign(env, extra);
|
|
863
|
+
return {
|
|
864
|
+
id: agent.id,
|
|
865
|
+
preset: agent.preset,
|
|
866
|
+
command: agent.command,
|
|
867
|
+
env,
|
|
868
|
+
startupTimeoutMs: table?.startup_timeout ?? 60000,
|
|
869
|
+
authMethod: table?.auth_method ?? null
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/config/loader.ts
|
|
874
|
+
import { existsSync, readFileSync } from "fs";
|
|
875
|
+
import { dirname as dirname2 } from "path";
|
|
876
|
+
import { parse, TomlError } from "smol-toml";
|
|
877
|
+
|
|
878
|
+
// src/config/introspect.ts
|
|
879
|
+
function defOf(schema) {
|
|
880
|
+
return schema._zod.def;
|
|
881
|
+
}
|
|
882
|
+
function unwrap(schema) {
|
|
883
|
+
let current = schema;
|
|
884
|
+
for (;; ) {
|
|
885
|
+
const def = defOf(current);
|
|
886
|
+
if (def.innerType && ["optional", "default", "prefault", "nullable", "readonly"].includes(def.type)) {
|
|
887
|
+
current = def.innerType;
|
|
888
|
+
} else if (def.type === "pipe" && def.in) {
|
|
889
|
+
current = def.in;
|
|
890
|
+
} else {
|
|
891
|
+
return current;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function schemaAt(schema, path) {
|
|
896
|
+
let current = schema;
|
|
897
|
+
for (const segment of path) {
|
|
898
|
+
if (!current)
|
|
899
|
+
return;
|
|
900
|
+
const def = defOf(unwrap(current));
|
|
901
|
+
if (def.type === "object")
|
|
902
|
+
current = def.shape?.[String(segment)];
|
|
903
|
+
else if (def.type === "record")
|
|
904
|
+
current = def.valueType;
|
|
905
|
+
else if (def.type === "array")
|
|
906
|
+
current = def.element;
|
|
907
|
+
else
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
return current;
|
|
911
|
+
}
|
|
912
|
+
function objectKeys(schema) {
|
|
913
|
+
if (!schema)
|
|
914
|
+
return [];
|
|
915
|
+
const def = defOf(unwrap(schema));
|
|
916
|
+
return def.type === "object" ? Object.keys(def.shape ?? {}) : [];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/config/schema.ts
|
|
920
|
+
import { cpus, hostname } from "os";
|
|
921
|
+
import { z as z9 } from "zod";
|
|
755
922
|
|
|
756
923
|
// ../protocol/src/capabilities.ts
|
|
757
924
|
import { z as z2 } from "zod";
|
|
@@ -1886,175 +2053,6 @@ var WorkAssignment = z8.looseObject({
|
|
|
1886
2053
|
var ClaimResponse = z8.looseObject({
|
|
1887
2054
|
assignment: WorkAssignment.nullable()
|
|
1888
2055
|
});
|
|
1889
|
-
// src/config/resolve.ts
|
|
1890
|
-
function agentById(config, id) {
|
|
1891
|
-
const table = config.agents[id];
|
|
1892
|
-
if (table) {
|
|
1893
|
-
const base = table.preset ? [...AGENT_PRESETS[table.preset].command] : table.command ?? [];
|
|
1894
|
-
return {
|
|
1895
|
-
id,
|
|
1896
|
-
preset: table.preset ?? null,
|
|
1897
|
-
command: [...base, ...table.args],
|
|
1898
|
-
env: table.env,
|
|
1899
|
-
maxParallel: table.max_parallel
|
|
1900
|
-
};
|
|
1901
|
-
}
|
|
1902
|
-
if (isPreset(id)) {
|
|
1903
|
-
return {
|
|
1904
|
-
id,
|
|
1905
|
-
preset: id,
|
|
1906
|
-
command: [...AGENT_PRESETS[id].command],
|
|
1907
|
-
env: {},
|
|
1908
|
-
maxParallel: undefined
|
|
1909
|
-
};
|
|
1910
|
-
}
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
function playedRoles(config) {
|
|
1914
|
-
return config.runner.roles.flatMap((role) => {
|
|
1915
|
-
const table = config.roles[role];
|
|
1916
|
-
return table ? [[role, table]] : [];
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
function resolveAgents(config) {
|
|
1920
|
-
const byId = new Map;
|
|
1921
|
-
for (const id of Object.keys(config.agents)) {
|
|
1922
|
-
const agent = agentById(config, id);
|
|
1923
|
-
if (agent)
|
|
1924
|
-
byId.set(id, { ...agent, roles: [] });
|
|
1925
|
-
}
|
|
1926
|
-
for (const [role, table] of playedRoles(config)) {
|
|
1927
|
-
const known = byId.get(table.agent);
|
|
1928
|
-
if (known) {
|
|
1929
|
-
if (!known.roles.includes(role))
|
|
1930
|
-
known.roles.push(role);
|
|
1931
|
-
continue;
|
|
1932
|
-
}
|
|
1933
|
-
const agent = agentById(config, table.agent);
|
|
1934
|
-
if (agent)
|
|
1935
|
-
byId.set(table.agent, { ...agent, roles: [role] });
|
|
1936
|
-
}
|
|
1937
|
-
return [...byId.values()];
|
|
1938
|
-
}
|
|
1939
|
-
function repositoryName(repo) {
|
|
1940
|
-
if (repo.name)
|
|
1941
|
-
return repo.name;
|
|
1942
|
-
const source = repo.remote ? normalizeRemote(repo.remote) : repo.path;
|
|
1943
|
-
return basename(source.replace(/\/+$/, "")) || "repository";
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
// src/acp/presets.ts
|
|
1947
|
-
var SESSION_VARIABLES = new Set([
|
|
1948
|
-
"HYPOTENUSE_SESSION_ID",
|
|
1949
|
-
"HYPOTENUSE_LINEAR_SESSION_ID",
|
|
1950
|
-
"HYPOTENUSE_ISSUE",
|
|
1951
|
-
"HYPOTENUSE_ISSUE_URL",
|
|
1952
|
-
"HYPOTENUSE_BRANCH",
|
|
1953
|
-
"HYPOTENUSE_BASE_BRANCH",
|
|
1954
|
-
"HYPOTENUSE_ROLE",
|
|
1955
|
-
"HYPOTENUSE_SESSION_URL"
|
|
1956
|
-
]);
|
|
1957
|
-
function expandVariables(value, env) {
|
|
1958
|
-
return value.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_, braced, bare) => env[braced ?? bare] ?? "");
|
|
1959
|
-
}
|
|
1960
|
-
function sessionVariables(assignment, branch) {
|
|
1961
|
-
return {
|
|
1962
|
-
HYPOTENUSE_SESSION_ID: String(assignment.agentSessionId),
|
|
1963
|
-
HYPOTENUSE_LINEAR_SESSION_ID: assignment.externalSessionId,
|
|
1964
|
-
HYPOTENUSE_ISSUE: assignment.issue.identifier ?? "",
|
|
1965
|
-
HYPOTENUSE_ISSUE_URL: assignment.issue.url ?? "",
|
|
1966
|
-
HYPOTENUSE_BRANCH: branch ?? "",
|
|
1967
|
-
HYPOTENUSE_BASE_BRANCH: assignment.repository?.baseBranch ?? "",
|
|
1968
|
-
HYPOTENUSE_ROLE: assignment.role,
|
|
1969
|
-
HYPOTENUSE_SESSION_URL: assignment.dashboardUrl,
|
|
1970
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
1971
|
-
};
|
|
1972
|
-
}
|
|
1973
|
-
function sessionEnvironment(daemonEnv, extra) {
|
|
1974
|
-
const env = {};
|
|
1975
|
-
for (const [key, value] of Object.entries(daemonEnv)) {
|
|
1976
|
-
if (value === undefined)
|
|
1977
|
-
continue;
|
|
1978
|
-
if (key.startsWith("HYPOTENUSE_") && !SESSION_VARIABLES.has(key))
|
|
1979
|
-
continue;
|
|
1980
|
-
env[key] = value;
|
|
1981
|
-
}
|
|
1982
|
-
return Object.assign(env, extra);
|
|
1983
|
-
}
|
|
1984
|
-
function resolveAgentLaunch(config, agentId, daemonEnv, extra = {}) {
|
|
1985
|
-
const agent = resolveAgents(config).find((a) => a.id === agentId);
|
|
1986
|
-
if (!agent || !agent.command.length)
|
|
1987
|
-
return null;
|
|
1988
|
-
const table = config.agents[agentId];
|
|
1989
|
-
const env = sessionEnvironment(daemonEnv, {});
|
|
1990
|
-
const preset = agent.preset ? AGENT_PRESETS[agent.preset] : null;
|
|
1991
|
-
if (preset && "env" in preset && preset.env)
|
|
1992
|
-
Object.assign(env, preset.env);
|
|
1993
|
-
if (agent.command[0] === "npx")
|
|
1994
|
-
env.npm_config_yes = "true";
|
|
1995
|
-
for (const [key, value] of Object.entries(agent.env))
|
|
1996
|
-
env[key] = expandVariables(value, daemonEnv);
|
|
1997
|
-
Object.assign(env, extra);
|
|
1998
|
-
return {
|
|
1999
|
-
id: agent.id,
|
|
2000
|
-
preset: agent.preset,
|
|
2001
|
-
command: agent.command,
|
|
2002
|
-
env,
|
|
2003
|
-
startupTimeoutMs: table?.startup_timeout ?? 60000,
|
|
2004
|
-
authMethod: table?.auth_method ?? null
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// src/config/loader.ts
|
|
2009
|
-
import { existsSync, readFileSync } from "fs";
|
|
2010
|
-
import { dirname as dirname2 } from "path";
|
|
2011
|
-
import { parse, TomlError } from "smol-toml";
|
|
2012
|
-
|
|
2013
|
-
// src/config/introspect.ts
|
|
2014
|
-
function defOf(schema) {
|
|
2015
|
-
return schema._zod.def;
|
|
2016
|
-
}
|
|
2017
|
-
function unwrap(schema) {
|
|
2018
|
-
let current = schema;
|
|
2019
|
-
for (;; ) {
|
|
2020
|
-
const def = defOf(current);
|
|
2021
|
-
if (def.innerType && ["optional", "default", "prefault", "nullable", "readonly"].includes(def.type)) {
|
|
2022
|
-
current = def.innerType;
|
|
2023
|
-
} else if (def.type === "pipe" && def.in) {
|
|
2024
|
-
current = def.in;
|
|
2025
|
-
} else {
|
|
2026
|
-
return current;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
function schemaAt(schema, path) {
|
|
2031
|
-
let current = schema;
|
|
2032
|
-
for (const segment of path) {
|
|
2033
|
-
if (!current)
|
|
2034
|
-
return;
|
|
2035
|
-
const def = defOf(unwrap(current));
|
|
2036
|
-
if (def.type === "object")
|
|
2037
|
-
current = def.shape?.[String(segment)];
|
|
2038
|
-
else if (def.type === "record")
|
|
2039
|
-
current = def.valueType;
|
|
2040
|
-
else if (def.type === "array")
|
|
2041
|
-
current = def.element;
|
|
2042
|
-
else
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
return current;
|
|
2046
|
-
}
|
|
2047
|
-
function objectKeys(schema) {
|
|
2048
|
-
if (!schema)
|
|
2049
|
-
return [];
|
|
2050
|
-
const def = defOf(unwrap(schema));
|
|
2051
|
-
return def.type === "object" ? Object.keys(def.shape ?? {}) : [];
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
// src/config/schema.ts
|
|
2055
|
-
import { cpus, hostname } from "os";
|
|
2056
|
-
import { z as z9 } from "zod";
|
|
2057
|
-
|
|
2058
2056
|
// src/config/durations.ts
|
|
2059
2057
|
var DURATION_UNITS = {
|
|
2060
2058
|
ms: 1,
|
|
@@ -2252,7 +2250,6 @@ function buildConfigSchema(ctx) {
|
|
|
2252
2250
|
const repository = z9.strictObject({
|
|
2253
2251
|
name: z9.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`").max(64).optional().describe("Unique; used in worktree paths"),
|
|
2254
2252
|
path: path().describe("An existing git clone"),
|
|
2255
|
-
remote: z9.string().trim().min(1).optional().describe("The repository's remote; must match origin"),
|
|
2256
2253
|
base_branch: z9.string().trim().min(1).optional().describe("The branch worktrees start from"),
|
|
2257
2254
|
setup: strings().default([]).describe("Shell commands run in each new worktree"),
|
|
2258
2255
|
setup_timeout: duration().prefault("10m").describe("Per setup command"),
|
|
@@ -2592,11 +2589,11 @@ function originHead(machine, path) {
|
|
|
2592
2589
|
return result.code === 0 && ref.startsWith("origin/") ? ref.slice("origin/".length) : null;
|
|
2593
2590
|
}
|
|
2594
2591
|
function resolveRepository(machine, repo) {
|
|
2595
|
-
const remote =
|
|
2592
|
+
const remote = originOf(machine, repo.path);
|
|
2596
2593
|
if (!remote)
|
|
2597
2594
|
return null;
|
|
2598
2595
|
return {
|
|
2599
|
-
name: repositoryName(
|
|
2596
|
+
name: repositoryName(repo, remote),
|
|
2600
2597
|
remote,
|
|
2601
2598
|
baseBranch: repo.base_branch ?? originHead(machine, repo.path) ?? "main",
|
|
2602
2599
|
path: repo.path
|
|
@@ -3140,35 +3137,7 @@ function validateReferences(config) {
|
|
|
3140
3137
|
hint: 'the default denies edit, delete, move, and execute: { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }'
|
|
3141
3138
|
});
|
|
3142
3139
|
}
|
|
3143
|
-
const names = new Map;
|
|
3144
|
-
const remotes = new Map;
|
|
3145
3140
|
config.repositories.forEach((repo, index) => {
|
|
3146
|
-
const name = repositoryName(repo);
|
|
3147
|
-
if (!repo.name && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
|
|
3148
|
-
errors.push({
|
|
3149
|
-
path: `repositories[${index}]`,
|
|
3150
|
-
message: `"${name}" is not a usable repository name (letters, digits, \`.\`, \`_\`, \`-\`); set name`
|
|
3151
|
-
});
|
|
3152
|
-
}
|
|
3153
|
-
const firstName = names.get(name);
|
|
3154
|
-
if (firstName !== undefined) {
|
|
3155
|
-
errors.push({
|
|
3156
|
-
path: `repositories[${index}].name`,
|
|
3157
|
-
message: `"${name}" is already repositories[${firstName}]`
|
|
3158
|
-
});
|
|
3159
|
-
} else
|
|
3160
|
-
names.set(name, index);
|
|
3161
|
-
if (repo.remote) {
|
|
3162
|
-
const remote = normalizeRemote(repo.remote);
|
|
3163
|
-
const first = remotes.get(remote);
|
|
3164
|
-
if (first !== undefined) {
|
|
3165
|
-
errors.push({
|
|
3166
|
-
path: `repositories[${index}].remote`,
|
|
3167
|
-
message: `${remote} is already repositories[${first}]`
|
|
3168
|
-
});
|
|
3169
|
-
} else
|
|
3170
|
-
remotes.set(remote, index);
|
|
3171
|
-
}
|
|
3172
3141
|
if (repo.cleanup === "ttl" && config.sessions.cleanup_ttl === undefined) {
|
|
3173
3142
|
errors.push({
|
|
3174
3143
|
path: `repositories[${index}].cleanup`,
|
|
@@ -3259,6 +3228,8 @@ function validateMachine(config, machine, options = {}) {
|
|
|
3259
3228
|
message: `git ${version.join(".")} is older than ${MIN_GIT_VERSION.join(".")}`
|
|
3260
3229
|
});
|
|
3261
3230
|
}
|
|
3231
|
+
const origins = new Map;
|
|
3232
|
+
const names = new Map;
|
|
3262
3233
|
config.repositories.forEach((repo, index) => {
|
|
3263
3234
|
const at = `repositories[${index}]`;
|
|
3264
3235
|
if (!machine.isDirectory(repo.path)) {
|
|
@@ -3285,21 +3256,37 @@ function validateMachine(config, machine, options = {}) {
|
|
|
3285
3256
|
return;
|
|
3286
3257
|
}
|
|
3287
3258
|
const origin = originOf(machine, repo.path);
|
|
3288
|
-
const configured = repo.remote ? normalizeRemote(repo.remote) : null;
|
|
3289
3259
|
if (!origin) {
|
|
3290
3260
|
errors.push({
|
|
3291
|
-
path: `${at}.
|
|
3261
|
+
path: `${at}.path`,
|
|
3292
3262
|
message: `${home(repo.path)} has no origin remote`
|
|
3293
3263
|
});
|
|
3294
3264
|
return;
|
|
3295
3265
|
}
|
|
3296
|
-
|
|
3266
|
+
const first = origins.get(origin);
|
|
3267
|
+
if (first !== undefined) {
|
|
3297
3268
|
errors.push({
|
|
3298
|
-
path: `${at}.
|
|
3299
|
-
message:
|
|
3269
|
+
path: `${at}.path`,
|
|
3270
|
+
message: `${home(repo.path)} is another clone of ${origin}, already repositories[${first}]`
|
|
3300
3271
|
});
|
|
3301
3272
|
return;
|
|
3302
3273
|
}
|
|
3274
|
+
origins.set(origin, index);
|
|
3275
|
+
const name = repositoryName(repo, origin);
|
|
3276
|
+
if (!repo.name && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
|
|
3277
|
+
errors.push({
|
|
3278
|
+
path: `${at}`,
|
|
3279
|
+
message: `"${name}" (from origin ${origin}) is not a usable repository name (letters, digits, \`.\`, \`_\`, \`-\`); set name`
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
const firstName = names.get(name);
|
|
3283
|
+
if (firstName !== undefined) {
|
|
3284
|
+
errors.push({
|
|
3285
|
+
path: `${at}.name`,
|
|
3286
|
+
message: `"${name}" is already repositories[${firstName}]${repo.name ? "" : ` (from origin ${origin}); set name`}`
|
|
3287
|
+
});
|
|
3288
|
+
} else
|
|
3289
|
+
names.set(name, index);
|
|
3303
3290
|
const base = repo.base_branch ?? originHead(machine, repo.path) ?? "main";
|
|
3304
3291
|
if (!options.offline) {
|
|
3305
3292
|
const heads = machine.git(["ls-remote", "--heads", "origin", base], repo.path);
|
|
@@ -3935,9 +3922,9 @@ async function socketAnswers(socketPath, timeoutMs = 1500) {
|
|
|
3935
3922
|
import { spawn as spawn3 } from "child_process";
|
|
3936
3923
|
import {
|
|
3937
3924
|
closeSync as closeSync3,
|
|
3938
|
-
existsSync as
|
|
3925
|
+
existsSync as existsSync18,
|
|
3939
3926
|
openSync as openSync3,
|
|
3940
|
-
readFileSync as
|
|
3927
|
+
readFileSync as readFileSync12,
|
|
3941
3928
|
readSync,
|
|
3942
3929
|
rmSync as rmSync9,
|
|
3943
3930
|
statSync as statSync5
|
|
@@ -4172,8 +4159,8 @@ class DaemonLog {
|
|
|
4172
4159
|
|
|
4173
4160
|
// src/daemon/main.ts
|
|
4174
4161
|
import { spawn as spawn2 } from "child_process";
|
|
4175
|
-
import { createHash as
|
|
4176
|
-
import { existsSync as
|
|
4162
|
+
import { createHash as createHash3 } from "crypto";
|
|
4163
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11, rmSync as rmSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
4177
4164
|
import { join as join10 } from "path";
|
|
4178
4165
|
|
|
4179
4166
|
// src/acp/host.ts
|
|
@@ -4634,7 +4621,7 @@ function coAuthorEnvironment(env, hooksDir, coAuthor) {
|
|
|
4634
4621
|
|
|
4635
4622
|
// src/worktree/manager.ts
|
|
4636
4623
|
import { existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
4637
|
-
import { join as join6 } from "path";
|
|
4624
|
+
import { isAbsolute as isAbsolute5, join as join6, relative as relative4, resolve as resolve2 } from "path";
|
|
4638
4625
|
|
|
4639
4626
|
// src/worktree/branch.ts
|
|
4640
4627
|
var SLUG_MAX = 40;
|
|
@@ -5187,6 +5174,10 @@ ${stderrTail(fetched)}
|
|
|
5187
5174
|
return { row: previous, reused: true };
|
|
5188
5175
|
}
|
|
5189
5176
|
request.output(`not reusing ${previous.path}: HEAD is ${head.stdout.trim() || "unreadable"}, not ${previous.branch}`);
|
|
5177
|
+
} else {
|
|
5178
|
+
const orphan = await this.ownOrphan(request, repository);
|
|
5179
|
+
if (orphan)
|
|
5180
|
+
return { row: orphan, reused: true };
|
|
5190
5181
|
}
|
|
5191
5182
|
if (!this.diskStatus(request.config).enough) {
|
|
5192
5183
|
if (!cleaned)
|
|
@@ -5237,6 +5228,45 @@ ${stderrTail(fetched)}
|
|
|
5237
5228
|
return { row: created, reused: false };
|
|
5238
5229
|
});
|
|
5239
5230
|
}
|
|
5231
|
+
async ownOrphan(request, repository) {
|
|
5232
|
+
const id = request.assignment.agentSessionId;
|
|
5233
|
+
const root = resolve2(request.config.sessions.worktree_root);
|
|
5234
|
+
const list = await this.git(["worktree", "list", "--porcelain"], {
|
|
5235
|
+
cwd: repository.path
|
|
5236
|
+
});
|
|
5237
|
+
if (list.code !== 0)
|
|
5238
|
+
return null;
|
|
5239
|
+
for (const block of list.stdout.split(`
|
|
5240
|
+
|
|
5241
|
+
`)) {
|
|
5242
|
+
const path = /^worktree (.+)$/m.exec(block)?.[1];
|
|
5243
|
+
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
5244
|
+
if (!path || !branch || !existsSync10(path))
|
|
5245
|
+
continue;
|
|
5246
|
+
const rel = relative4(root, path);
|
|
5247
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute5(rel))
|
|
5248
|
+
continue;
|
|
5249
|
+
if (await this.branchOwner(repository.path, branch) !== id)
|
|
5250
|
+
continue;
|
|
5251
|
+
const known = this.state.worktreeAt(path);
|
|
5252
|
+
if (known && (known.state === "removed" || known.agentSessionId !== id))
|
|
5253
|
+
continue;
|
|
5254
|
+
const row = known ?? this.state.insertWorktree({
|
|
5255
|
+
kind: "worktree",
|
|
5256
|
+
repository: repository.name,
|
|
5257
|
+
repoPath: repository.path,
|
|
5258
|
+
path,
|
|
5259
|
+
branch,
|
|
5260
|
+
baseBranch: repository.baseBranch,
|
|
5261
|
+
agentSessionId: id
|
|
5262
|
+
});
|
|
5263
|
+
this.state.updateWorktree(row.id, { state: "active", touch: true });
|
|
5264
|
+
this.state.linkWorktree(id, row.id);
|
|
5265
|
+
request.output(`reusing ${path} on ${branch}: a previous daemon made it but did not record it`);
|
|
5266
|
+
return this.state.worktree(row.id) ?? row;
|
|
5267
|
+
}
|
|
5268
|
+
return null;
|
|
5269
|
+
}
|
|
5240
5270
|
reusable(agentSessionId) {
|
|
5241
5271
|
const session = this.state.session(agentSessionId);
|
|
5242
5272
|
const row = session?.worktreeId ? this.state.worktree(session.worktreeId) : null;
|
|
@@ -5582,7 +5612,7 @@ import {
|
|
|
5582
5612
|
rmSync as rmSync4,
|
|
5583
5613
|
writeFileSync as writeFileSync4
|
|
5584
5614
|
} from "fs";
|
|
5585
|
-
import { basename as basename2, dirname as dirname7, isAbsolute as
|
|
5615
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute6, join as join7, resolve as resolve3, sep } from "path";
|
|
5586
5616
|
|
|
5587
5617
|
class OutsideWorktree extends Error {
|
|
5588
5618
|
path;
|
|
@@ -5641,7 +5671,7 @@ class ConfinedFs {
|
|
|
5641
5671
|
const root = this.roots[0];
|
|
5642
5672
|
if (!root)
|
|
5643
5673
|
throw new OutsideWorktree(path);
|
|
5644
|
-
return
|
|
5674
|
+
return isAbsolute6(path) ? resolve3(path) : resolve3(root, path);
|
|
5645
5675
|
}
|
|
5646
5676
|
read(path, line, limit) {
|
|
5647
5677
|
const real = this.resolveExisting(path);
|
|
@@ -5935,7 +5965,7 @@ function callText(call) {
|
|
|
5935
5965
|
}
|
|
5936
5966
|
|
|
5937
5967
|
// src/acp/mcp-servers.ts
|
|
5938
|
-
import { isAbsolute as
|
|
5968
|
+
import { isAbsolute as isAbsolute7 } from "path";
|
|
5939
5969
|
var HYPOTENUSE_SERVER_NAME = "hypotenuse";
|
|
5940
5970
|
var VARIABLE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
5941
5971
|
|
|
@@ -5985,7 +6015,7 @@ function turnMcpServers(role, caps, env, hypotenuse) {
|
|
|
5985
6015
|
const [command, ...args] = s.command;
|
|
5986
6016
|
servers.push({
|
|
5987
6017
|
name: s.name,
|
|
5988
|
-
command:
|
|
6018
|
+
command: isAbsolute7(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
|
|
5989
6019
|
args,
|
|
5990
6020
|
env: pairs(s.name, s.env, env)
|
|
5991
6021
|
});
|
|
@@ -7032,11 +7062,10 @@ class Turn {
|
|
|
7032
7062
|
return stop === "end_turn" ? this.result("completed") : this.result("failed", `stopReason: ${stop}`);
|
|
7033
7063
|
}
|
|
7034
7064
|
repositoryName() {
|
|
7065
|
+
if (this.workspace?.repository)
|
|
7066
|
+
return this.workspace.repository.name;
|
|
7035
7067
|
const remote = this.ctx.assignment.repository?.remote;
|
|
7036
|
-
|
|
7037
|
-
return null;
|
|
7038
|
-
const repo = this.ctx.config.repositories.find((r) => r.remote && r.remote.includes(remote));
|
|
7039
|
-
return repo ? repositoryName(repo) : remote.split("/").pop() ?? null;
|
|
7068
|
+
return remote ? remote.split("/").pop() ?? null : null;
|
|
7040
7069
|
}
|
|
7041
7070
|
async stopped() {
|
|
7042
7071
|
const reason = this.endReason ?? "stop";
|
|
@@ -7279,9 +7308,131 @@ function registrationHeartbeat() {
|
|
|
7279
7308
|
};
|
|
7280
7309
|
}
|
|
7281
7310
|
|
|
7311
|
+
// src/daemon/config-restart.ts
|
|
7312
|
+
class ConfigRestart {
|
|
7313
|
+
deps;
|
|
7314
|
+
timeoutMs;
|
|
7315
|
+
pending = null;
|
|
7316
|
+
generation = 0;
|
|
7317
|
+
takenOver = false;
|
|
7318
|
+
constructor(deps, timeoutMs) {
|
|
7319
|
+
this.deps = deps;
|
|
7320
|
+
this.timeoutMs = timeoutMs;
|
|
7321
|
+
}
|
|
7322
|
+
get isPending() {
|
|
7323
|
+
return this.pending !== null;
|
|
7324
|
+
}
|
|
7325
|
+
get isTakenOver() {
|
|
7326
|
+
return this.takenOver;
|
|
7327
|
+
}
|
|
7328
|
+
saved(outcome, path) {
|
|
7329
|
+
if (this.takenOver)
|
|
7330
|
+
return null;
|
|
7331
|
+
if (this.pending !== null) {
|
|
7332
|
+
if (!outcome.needsRestart) {
|
|
7333
|
+
this.callOff(outcome.invalid ? `${path} is not valid now; the running config stays` : "the change that needed it was undone");
|
|
7334
|
+
}
|
|
7335
|
+
return null;
|
|
7336
|
+
}
|
|
7337
|
+
if (!outcome.needsRestart)
|
|
7338
|
+
return null;
|
|
7339
|
+
const problem = this.deps.startProblem();
|
|
7340
|
+
if (problem) {
|
|
7341
|
+
this.deps.log(`config restart not started: the daemon would not start on ${path} as it is now (${problem}); the running config stays`);
|
|
7342
|
+
return null;
|
|
7343
|
+
}
|
|
7344
|
+
const generation = ++this.generation;
|
|
7345
|
+
this.pending = generation;
|
|
7346
|
+
this.deps.pauseClaims();
|
|
7347
|
+
return this.wait(generation, path);
|
|
7348
|
+
}
|
|
7349
|
+
takeOver() {
|
|
7350
|
+
this.takenOver = true;
|
|
7351
|
+
if (this.pending === null)
|
|
7352
|
+
return;
|
|
7353
|
+
this.pending = null;
|
|
7354
|
+
this.deps.log("config restart called off: a stop or restart was asked for");
|
|
7355
|
+
}
|
|
7356
|
+
async wait(generation, path) {
|
|
7357
|
+
const idle = await this.deps.waitIdle(this.timeoutMs);
|
|
7358
|
+
if (!this.current(generation))
|
|
7359
|
+
return;
|
|
7360
|
+
const problem = this.deps.startProblem();
|
|
7361
|
+
if (problem) {
|
|
7362
|
+
this.callOff(`the daemon would not start on ${path} as it is now (${problem}); the running config stays`);
|
|
7363
|
+
return;
|
|
7364
|
+
}
|
|
7365
|
+
if (!idle)
|
|
7366
|
+
await this.deps.stopTurns();
|
|
7367
|
+
if (!this.current(generation))
|
|
7368
|
+
return;
|
|
7369
|
+
this.pending = null;
|
|
7370
|
+
this.deps.restart();
|
|
7371
|
+
}
|
|
7372
|
+
current(generation) {
|
|
7373
|
+
return this.pending === generation && !this.takenOver && !this.deps.exiting();
|
|
7374
|
+
}
|
|
7375
|
+
callOff(reason) {
|
|
7376
|
+
if (this.pending === null)
|
|
7377
|
+
return;
|
|
7378
|
+
this.pending = null;
|
|
7379
|
+
this.deps.resumeClaims();
|
|
7380
|
+
this.deps.log(`config restart called off: ${reason}`);
|
|
7381
|
+
}
|
|
7382
|
+
}
|
|
7383
|
+
|
|
7384
|
+
// src/daemon/config-watch.ts
|
|
7385
|
+
import { createHash as createHash2 } from "crypto";
|
|
7386
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9, watch } from "fs";
|
|
7387
|
+
import { basename as basename3, dirname as dirname8 } from "path";
|
|
7388
|
+
var CONFIG_WATCH_DEBOUNCE_MS = 300;
|
|
7389
|
+
function digest(path) {
|
|
7390
|
+
if (!existsSync13(path))
|
|
7391
|
+
return null;
|
|
7392
|
+
try {
|
|
7393
|
+
return createHash2("sha256").update(readFileSync9(path)).digest("hex");
|
|
7394
|
+
} catch {
|
|
7395
|
+
return null;
|
|
7396
|
+
}
|
|
7397
|
+
}
|
|
7398
|
+
function watchConfigFile(path, onChange, options = {}) {
|
|
7399
|
+
const name = basename3(path);
|
|
7400
|
+
let last = digest(path);
|
|
7401
|
+
let timer = null;
|
|
7402
|
+
let watcher;
|
|
7403
|
+
const settle = () => {
|
|
7404
|
+
timer = null;
|
|
7405
|
+
const now = digest(path);
|
|
7406
|
+
if (now === null || now === last)
|
|
7407
|
+
return;
|
|
7408
|
+
last = now;
|
|
7409
|
+
onChange();
|
|
7410
|
+
};
|
|
7411
|
+
try {
|
|
7412
|
+
watcher = watch(dirname8(path), (_event, file) => {
|
|
7413
|
+
if (file !== null && file !== name)
|
|
7414
|
+
return;
|
|
7415
|
+
if (timer)
|
|
7416
|
+
clearTimeout(timer);
|
|
7417
|
+
timer = setTimeout(settle, options.debounceMs ?? CONFIG_WATCH_DEBOUNCE_MS);
|
|
7418
|
+
});
|
|
7419
|
+
} catch (error) {
|
|
7420
|
+
options.onError?.(error);
|
|
7421
|
+
return null;
|
|
7422
|
+
}
|
|
7423
|
+
watcher.on("error", (error) => options.onError?.(error));
|
|
7424
|
+
return {
|
|
7425
|
+
close() {
|
|
7426
|
+
if (timer)
|
|
7427
|
+
clearTimeout(timer);
|
|
7428
|
+
watcher.close();
|
|
7429
|
+
}
|
|
7430
|
+
};
|
|
7431
|
+
}
|
|
7432
|
+
|
|
7282
7433
|
// src/daemon/control-socket.ts
|
|
7283
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
7284
|
-
import { dirname as
|
|
7434
|
+
import { chmodSync as chmodSync4, existsSync as existsSync14, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
|
|
7435
|
+
import { dirname as dirname9 } from "path";
|
|
7285
7436
|
var METHOD_NOT_FOUND = -32601;
|
|
7286
7437
|
var INVALID_PARAMS = -32602;
|
|
7287
7438
|
var INTERNAL_ERROR = -32603;
|
|
@@ -7306,8 +7457,8 @@ class ControlServer {
|
|
|
7306
7457
|
this.onError = onError;
|
|
7307
7458
|
}
|
|
7308
7459
|
listen() {
|
|
7309
|
-
mkdirSync8(
|
|
7310
|
-
if (
|
|
7460
|
+
mkdirSync8(dirname9(this.path), { recursive: true, mode: 448 });
|
|
7461
|
+
if (existsSync14(this.path))
|
|
7311
7462
|
rmSync5(this.path, { force: true });
|
|
7312
7463
|
const previous = process.umask(127);
|
|
7313
7464
|
try {
|
|
@@ -7473,11 +7624,11 @@ import {
|
|
|
7473
7624
|
closeSync as closeSync2,
|
|
7474
7625
|
mkdirSync as mkdirSync9,
|
|
7475
7626
|
openSync as openSync2,
|
|
7476
|
-
readFileSync as
|
|
7627
|
+
readFileSync as readFileSync10,
|
|
7477
7628
|
rmSync as rmSync6,
|
|
7478
7629
|
writeSync
|
|
7479
7630
|
} from "fs";
|
|
7480
|
-
import { dirname as
|
|
7631
|
+
import { dirname as dirname10 } from "path";
|
|
7481
7632
|
var STARTING_WINDOW_MS = 30000;
|
|
7482
7633
|
function looksLikeDaemon(args) {
|
|
7483
7634
|
const program = args.findIndex((arg) => {
|
|
@@ -7489,7 +7640,7 @@ function looksLikeDaemon(args) {
|
|
|
7489
7640
|
function processArgs(pid) {
|
|
7490
7641
|
try {
|
|
7491
7642
|
if (process.platform === "linux") {
|
|
7492
|
-
return
|
|
7643
|
+
return readFileSync10(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
|
|
7493
7644
|
}
|
|
7494
7645
|
if (process.platform === "win32") {
|
|
7495
7646
|
const out = Bun.spawnSync([
|
|
@@ -7524,7 +7675,7 @@ var realProbe = {
|
|
|
7524
7675
|
function readLock(path) {
|
|
7525
7676
|
let text;
|
|
7526
7677
|
try {
|
|
7527
|
-
text =
|
|
7678
|
+
text = readFileSync10(path, "utf8");
|
|
7528
7679
|
} catch {
|
|
7529
7680
|
return;
|
|
7530
7681
|
}
|
|
@@ -7548,7 +7699,7 @@ async function inspectLock(path, probe = realProbe, now = Date.now) {
|
|
|
7548
7699
|
return now() - record.startedAt < STARTING_WINDOW_MS ? { kind: "starting", record } : { kind: "hung", record };
|
|
7549
7700
|
}
|
|
7550
7701
|
function writeExclusive(path, record) {
|
|
7551
|
-
mkdirSync9(
|
|
7702
|
+
mkdirSync9(dirname10(path), { recursive: true, mode: 448 });
|
|
7552
7703
|
let fd;
|
|
7553
7704
|
try {
|
|
7554
7705
|
fd = openSync2(path, "wx", 384);
|
|
@@ -7626,8 +7777,8 @@ function childArgs(args) {
|
|
|
7626
7777
|
|
|
7627
7778
|
// src/daemon/state.ts
|
|
7628
7779
|
import { Database } from "bun:sqlite";
|
|
7629
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
7630
|
-
import { dirname as
|
|
7780
|
+
import { chmodSync as chmodSync5, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
7781
|
+
import { dirname as dirname11 } from "path";
|
|
7631
7782
|
var LIVE_LOCAL_STATES = [
|
|
7632
7783
|
"claimed",
|
|
7633
7784
|
"running"
|
|
@@ -7749,9 +7900,9 @@ class StateDb {
|
|
|
7749
7900
|
this.path = path;
|
|
7750
7901
|
this.now = now;
|
|
7751
7902
|
if (path !== ":memory:") {
|
|
7752
|
-
mkdirSync10(
|
|
7903
|
+
mkdirSync10(dirname11(path), { recursive: true, mode: 448 });
|
|
7753
7904
|
}
|
|
7754
|
-
const fresh = path === ":memory:" || !
|
|
7905
|
+
const fresh = path === ":memory:" || !existsSync15(path);
|
|
7755
7906
|
this.db = new Database(path, { create: true, strict: true });
|
|
7756
7907
|
if (fresh && path !== ":memory:")
|
|
7757
7908
|
chmodSync5(path, 384);
|
|
@@ -8002,7 +8153,7 @@ class StateDb {
|
|
|
8002
8153
|
|
|
8003
8154
|
// src/daemon/supervisor.ts
|
|
8004
8155
|
import { hostname as hostname3 } from "os";
|
|
8005
|
-
import { isAbsolute as
|
|
8156
|
+
import { isAbsolute as isAbsolute8 } from "path";
|
|
8006
8157
|
|
|
8007
8158
|
// src/telemetry/collector.ts
|
|
8008
8159
|
var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
|
|
@@ -8569,8 +8720,8 @@ function resolvedConfigSecrets(config, env) {
|
|
|
8569
8720
|
}
|
|
8570
8721
|
|
|
8571
8722
|
// src/worktree/cleanup.ts
|
|
8572
|
-
import { existsSync as
|
|
8573
|
-
import { join as join9, relative as
|
|
8723
|
+
import { existsSync as existsSync16, readdirSync, rmSync as rmSync7 } from "fs";
|
|
8724
|
+
import { join as join9, relative as relative5, resolve as resolve4 } from "path";
|
|
8574
8725
|
|
|
8575
8726
|
// src/worktree/merge-state.ts
|
|
8576
8727
|
var MERGE_STATE_TTL_MS = 10 * 60000;
|
|
@@ -8707,14 +8858,14 @@ class WorktreeCleaner {
|
|
|
8707
8858
|
if (this.inUse(wt))
|
|
8708
8859
|
return { reason: "in use", overridable: [] };
|
|
8709
8860
|
const reasons = [];
|
|
8710
|
-
if (wt.kind === "worktree" &&
|
|
8861
|
+
if (wt.kind === "worktree" && existsSync16(wt.path)) {
|
|
8711
8862
|
const status = await this.git(["status", "--porcelain", "--", ".", `:(exclude)${KEEP_FILE}`], { cwd: wt.path });
|
|
8712
8863
|
if (status.code !== 0 || status.stdout.trim())
|
|
8713
8864
|
reasons.push("uncommitted changes");
|
|
8714
8865
|
if (await this.hasUnpushed(wt))
|
|
8715
8866
|
reasons.push("unpushed commits");
|
|
8716
8867
|
}
|
|
8717
|
-
if (
|
|
8868
|
+
if (existsSync16(join9(wt.path, KEEP_FILE)))
|
|
8718
8869
|
reasons.push("kept by request");
|
|
8719
8870
|
return { reason: reasons[0] ?? null, overridable: reasons };
|
|
8720
8871
|
}
|
|
@@ -8733,7 +8884,7 @@ class WorktreeCleaner {
|
|
|
8733
8884
|
return result.stdout.trim() !== "";
|
|
8734
8885
|
}
|
|
8735
8886
|
async adopt() {
|
|
8736
|
-
const root =
|
|
8887
|
+
const root = resolve4(this.config().sessions.worktree_root);
|
|
8737
8888
|
const known = new Set(this.state.worktrees({ live: true }).map((w) => w.path));
|
|
8738
8889
|
let adopted = 0;
|
|
8739
8890
|
for (const repo of this.options.repositories?.() ?? []) {
|
|
@@ -8749,7 +8900,7 @@ class WorktreeCleaner {
|
|
|
8749
8900
|
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
8750
8901
|
if (!path || !branch || known.has(path))
|
|
8751
8902
|
continue;
|
|
8752
|
-
const rel =
|
|
8903
|
+
const rel = relative5(root, path);
|
|
8753
8904
|
if (rel.startsWith("..") || rel === "")
|
|
8754
8905
|
continue;
|
|
8755
8906
|
const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
|
|
@@ -8771,7 +8922,7 @@ class WorktreeCleaner {
|
|
|
8771
8922
|
}
|
|
8772
8923
|
}
|
|
8773
8924
|
const scratch = join9(root, SCRATCH);
|
|
8774
|
-
if (
|
|
8925
|
+
if (existsSync16(scratch)) {
|
|
8775
8926
|
for (const name of readdirSync(scratch)) {
|
|
8776
8927
|
const path = join9(scratch, name);
|
|
8777
8928
|
const id = Number(name);
|
|
@@ -8825,7 +8976,7 @@ class WorktreeCleaner {
|
|
|
8825
8976
|
if (!options.dryRun)
|
|
8826
8977
|
this.state.updateWorktree(wt.id, { keptReason: reason });
|
|
8827
8978
|
};
|
|
8828
|
-
if (!
|
|
8979
|
+
if (!existsSync16(wt.path)) {
|
|
8829
8980
|
if (!options.dryRun) {
|
|
8830
8981
|
if (wt.repoPath)
|
|
8831
8982
|
await this.git(["worktree", "prune"], { cwd: wt.repoPath });
|
|
@@ -10339,6 +10490,14 @@ class Supervisor {
|
|
|
10339
10490
|
this.link.beatNow();
|
|
10340
10491
|
return { drained: this.slots.size === 0 && flushed, cancelled };
|
|
10341
10492
|
}
|
|
10493
|
+
pauseClaims() {
|
|
10494
|
+
this.draining = true;
|
|
10495
|
+
this.link.beatNow();
|
|
10496
|
+
}
|
|
10497
|
+
resumeClaims() {
|
|
10498
|
+
this.draining = false;
|
|
10499
|
+
this.link.beatNow();
|
|
10500
|
+
}
|
|
10342
10501
|
async waitIdle(timeoutMs) {
|
|
10343
10502
|
const deadline = Date.now() + timeoutMs;
|
|
10344
10503
|
while (this.slots.size > 0) {
|
|
@@ -10508,7 +10667,7 @@ function changedKeys(a, b, prefix = "") {
|
|
|
10508
10667
|
return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
|
|
10509
10668
|
}
|
|
10510
10669
|
function resolveCommand(command, env) {
|
|
10511
|
-
if (
|
|
10670
|
+
if (isAbsolute8(command))
|
|
10512
10671
|
return command;
|
|
10513
10672
|
return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
|
|
10514
10673
|
}
|
|
@@ -10564,18 +10723,19 @@ function revokedMarker(stateDir) {
|
|
|
10564
10723
|
return join10(stateDir, "revoked.json");
|
|
10565
10724
|
}
|
|
10566
10725
|
function fingerprint(token) {
|
|
10567
|
-
return
|
|
10726
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
10568
10727
|
}
|
|
10569
10728
|
function wasRevoked(stateDir, token) {
|
|
10570
10729
|
const file = revokedMarker(stateDir);
|
|
10571
|
-
if (!
|
|
10730
|
+
if (!existsSync17(file))
|
|
10572
10731
|
return false;
|
|
10573
10732
|
try {
|
|
10574
|
-
return JSON.parse(
|
|
10733
|
+
return JSON.parse(readFileSync11(file, "utf8")).token === fingerprint(token);
|
|
10575
10734
|
} catch {
|
|
10576
10735
|
return false;
|
|
10577
10736
|
}
|
|
10578
10737
|
}
|
|
10738
|
+
var CONFIG_RESTART_DRAIN_MS = 10 * 60000;
|
|
10579
10739
|
function readDaemonConfig(options, env) {
|
|
10580
10740
|
const path = resolveConfigPath(options.config, env);
|
|
10581
10741
|
const checked = checkConfig(path, { env, offline: true });
|
|
@@ -10648,19 +10808,55 @@ async function runDaemon(options) {
|
|
|
10648
10808
|
const machine = realMachine(env);
|
|
10649
10809
|
const api = new DaemonApi(origin, runner.token, (line) => log.debug(line));
|
|
10650
10810
|
let exiting = false;
|
|
10811
|
+
let restartScheduled = false;
|
|
10812
|
+
let configHandOff = null;
|
|
10651
10813
|
let supervisor;
|
|
10652
10814
|
let control;
|
|
10815
|
+
let watcher = null;
|
|
10653
10816
|
const teardown = async (drain) => {
|
|
10654
10817
|
if (drain) {
|
|
10655
10818
|
await supervisor.drain({ timeoutMs: 1e4, cancel: false });
|
|
10656
10819
|
} else {
|
|
10657
10820
|
await supervisor.sink.drain(2000);
|
|
10658
10821
|
}
|
|
10822
|
+
watcher?.close();
|
|
10659
10823
|
supervisor.close();
|
|
10660
10824
|
control.close();
|
|
10661
10825
|
releaseLock(paths.pidfile);
|
|
10662
10826
|
state.close();
|
|
10663
10827
|
};
|
|
10828
|
+
const restartSelf = (fromConfig = false) => {
|
|
10829
|
+
const service = env.HYPOTENUSE_SERVICE === "1";
|
|
10830
|
+
const answer = {
|
|
10831
|
+
restarting: true,
|
|
10832
|
+
mode: service ? "service" : "re-exec"
|
|
10833
|
+
};
|
|
10834
|
+
if (restartScheduled)
|
|
10835
|
+
return answer;
|
|
10836
|
+
restartScheduled = true;
|
|
10837
|
+
const handOff = setTimeout(() => {
|
|
10838
|
+
configHandOff = null;
|
|
10839
|
+
if (exiting)
|
|
10840
|
+
return;
|
|
10841
|
+
exiting = true;
|
|
10842
|
+
log.info(service ? "exiting for the service manager to restart" : "re-executing");
|
|
10843
|
+
teardown(false).then(() => {
|
|
10844
|
+
if (service)
|
|
10845
|
+
process.exit(EXIT_RESTART);
|
|
10846
|
+
const [command, ...prefix] = selfCommand();
|
|
10847
|
+
const child = spawn2(command, [...prefix, ...childArgs(process.argv.slice(2))], {
|
|
10848
|
+
detached: true,
|
|
10849
|
+
stdio: "ignore",
|
|
10850
|
+
env
|
|
10851
|
+
});
|
|
10852
|
+
child.unref();
|
|
10853
|
+
process.exit(0);
|
|
10854
|
+
});
|
|
10855
|
+
}, 50);
|
|
10856
|
+
if (fromConfig)
|
|
10857
|
+
configHandOff = handOff;
|
|
10858
|
+
return answer;
|
|
10859
|
+
};
|
|
10664
10860
|
const requestExit = (exit) => {
|
|
10665
10861
|
if (exiting)
|
|
10666
10862
|
return;
|
|
@@ -10696,6 +10892,31 @@ async function runDaemon(options) {
|
|
|
10696
10892
|
...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
|
|
10697
10893
|
]
|
|
10698
10894
|
});
|
|
10895
|
+
const configRestart = new ConfigRestart({
|
|
10896
|
+
pauseClaims: () => supervisor.pauseClaims(),
|
|
10897
|
+
resumeClaims: () => supervisor.resumeClaims(),
|
|
10898
|
+
waitIdle: (ms) => supervisor.waitIdle(ms),
|
|
10899
|
+
stopTurns: async () => {
|
|
10900
|
+
await supervisor.drain({ timeoutMs: 0, cancel: true });
|
|
10901
|
+
},
|
|
10902
|
+
startProblem: () => {
|
|
10903
|
+
const read = readDaemonConfig(options, env);
|
|
10904
|
+
if (!read.config)
|
|
10905
|
+
return "the config is not valid";
|
|
10906
|
+
const file = credentialsPath(env);
|
|
10907
|
+
try {
|
|
10908
|
+
resolveDaemonRunner(read.config, loadCredentials(file), file);
|
|
10909
|
+
} catch (error) {
|
|
10910
|
+
return error instanceof Error ? error.message : String(error);
|
|
10911
|
+
}
|
|
10912
|
+
return null;
|
|
10913
|
+
},
|
|
10914
|
+
exiting: () => exiting,
|
|
10915
|
+
restart: () => {
|
|
10916
|
+
restartSelf(true);
|
|
10917
|
+
},
|
|
10918
|
+
log: (message) => log.info(message)
|
|
10919
|
+
}, CONFIG_RESTART_DRAIN_MS);
|
|
10699
10920
|
control = new ControlServer(paths.socket, {
|
|
10700
10921
|
status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
|
|
10701
10922
|
"agents.list": (params) => supervisor.agents(Boolean(params.all)),
|
|
@@ -10713,30 +10934,23 @@ async function runDaemon(options) {
|
|
|
10713
10934
|
const report = await supervisor.cleaner.run(cleanupParams(params));
|
|
10714
10935
|
return report;
|
|
10715
10936
|
},
|
|
10716
|
-
drain: (params) =>
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10937
|
+
drain: (params) => {
|
|
10938
|
+
configRestart.takeOver();
|
|
10939
|
+
if (configHandOff) {
|
|
10940
|
+
clearTimeout(configHandOff);
|
|
10941
|
+
configHandOff = null;
|
|
10942
|
+
restartScheduled = false;
|
|
10943
|
+
log.info("config restart called off: a stop or restart was asked for");
|
|
10944
|
+
}
|
|
10945
|
+
return supervisor.drain({
|
|
10946
|
+
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : 600000,
|
|
10947
|
+
cancel: params.cancel === true
|
|
10948
|
+
});
|
|
10949
|
+
},
|
|
10720
10950
|
reload: () => supervisor.reload(),
|
|
10721
10951
|
restart: () => {
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
exiting = true;
|
|
10725
|
-
log.info(service ? "exiting for the service manager to restart" : "re-executing");
|
|
10726
|
-
teardown(false).then(() => {
|
|
10727
|
-
if (service)
|
|
10728
|
-
process.exit(EXIT_RESTART);
|
|
10729
|
-
const [command, ...prefix] = selfCommand();
|
|
10730
|
-
const child = spawn2(command, [...prefix, ...childArgs(process.argv.slice(2))], {
|
|
10731
|
-
detached: true,
|
|
10732
|
-
stdio: "ignore",
|
|
10733
|
-
env
|
|
10734
|
-
});
|
|
10735
|
-
child.unref();
|
|
10736
|
-
process.exit(0);
|
|
10737
|
-
});
|
|
10738
|
-
}, 50);
|
|
10739
|
-
return { restarting: true, mode: service ? "service" : "re-exec" };
|
|
10952
|
+
configRestart.takeOver();
|
|
10953
|
+
return restartSelf();
|
|
10740
10954
|
},
|
|
10741
10955
|
"mcp.hello": (params) => supervisor.broker.hello(params),
|
|
10742
10956
|
"mcp.call": (params) => supervisor.broker.call(params)
|
|
@@ -10772,6 +10986,19 @@ async function runDaemon(options) {
|
|
|
10772
10986
|
});
|
|
10773
10987
|
report({ ready: true, pid: process.pid });
|
|
10774
10988
|
rmSync8(revokedMarker(paths.stateDir), { force: true });
|
|
10989
|
+
watcher = watchConfigFile(paths.config, () => {
|
|
10990
|
+
if (exiting || restartScheduled || configRestart.isTakenOver)
|
|
10991
|
+
return;
|
|
10992
|
+
log.info(`${paths.config} changed; reloading`);
|
|
10993
|
+
const result = supervisor.reload();
|
|
10994
|
+
const needsRestart = !result.error && result.requiresRestart.length > 0;
|
|
10995
|
+
if (needsRestart && !configRestart.isPending) {
|
|
10996
|
+
log.info("restarting to apply the config change once running turns finish", { keys: result.requiresRestart.join(",") });
|
|
10997
|
+
}
|
|
10998
|
+
configRestart.saved({ needsRestart, invalid: Boolean(result.error) }, paths.config);
|
|
10999
|
+
}, {
|
|
11000
|
+
onError: (error) => log.warn(`not watching ${paths.config} for changes (\`hypotenuse daemon reload\` still applies them): ${error instanceof Error ? error.message : String(error)}`)
|
|
11001
|
+
});
|
|
10775
11002
|
}
|
|
10776
11003
|
|
|
10777
11004
|
// src/commands/daemon.ts
|
|
@@ -10933,22 +11160,22 @@ async function stopDaemon(ctx, options) {
|
|
|
10933
11160
|
}
|
|
10934
11161
|
async function tailLog(file, options) {
|
|
10935
11162
|
const keep = (line) => !options.session || line.includes(` session=${options.session}`);
|
|
10936
|
-
if (!
|
|
11163
|
+
if (!existsSync18(file)) {
|
|
10937
11164
|
if (!options.follow)
|
|
10938
11165
|
throw new Error(`${file} does not exist yet`);
|
|
10939
11166
|
} else {
|
|
10940
|
-
const all =
|
|
11167
|
+
const all = readFileSync12(file, "utf8").split(`
|
|
10941
11168
|
`).filter(Boolean).filter(keep);
|
|
10942
11169
|
for (const line of all.slice(-options.lines))
|
|
10943
11170
|
console.log(paintLogLine(line));
|
|
10944
11171
|
}
|
|
10945
11172
|
if (!options.follow)
|
|
10946
11173
|
return;
|
|
10947
|
-
let offset =
|
|
11174
|
+
let offset = existsSync18(file) ? statSync5(file).size : 0;
|
|
10948
11175
|
let partial = "";
|
|
10949
11176
|
for (;; ) {
|
|
10950
11177
|
await Bun.sleep(500);
|
|
10951
|
-
if (!
|
|
11178
|
+
if (!existsSync18(file))
|
|
10952
11179
|
continue;
|
|
10953
11180
|
const size = statSync5(file).size;
|
|
10954
11181
|
if (size < offset)
|
|
@@ -11043,7 +11270,7 @@ function registerDaemonCommands(program) {
|
|
|
11043
11270
|
const opts = this.opts();
|
|
11044
11271
|
const located = locateDaemon(ctx);
|
|
11045
11272
|
const sessionLog = opts.session && /^\d+$/.test(opts.session) ? sessionLogPath(located.stateDir, Number(opts.session)) : null;
|
|
11046
|
-
if (sessionLog &&
|
|
11273
|
+
if (sessionLog && existsSync18(sessionLog)) {
|
|
11047
11274
|
await tailLog(sessionLog, {
|
|
11048
11275
|
lines: Number(opts.lines) || 200,
|
|
11049
11276
|
follow: Boolean(opts.follow)
|
|
@@ -11509,13 +11736,14 @@ function registerCheckUpdateCommand(program) {
|
|
|
11509
11736
|
}
|
|
11510
11737
|
|
|
11511
11738
|
// src/commands/config.ts
|
|
11512
|
-
import { existsSync as
|
|
11739
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
11513
11740
|
import { hostname as hostname5 } from "os";
|
|
11514
|
-
import { dirname as
|
|
11741
|
+
import { dirname as dirname12, resolve as resolve5 } from "path";
|
|
11515
11742
|
|
|
11516
11743
|
// src/config/template.toml
|
|
11517
11744
|
var template_default = `#:schema https://www.hypotenuse.cloud/schemas/config-v1.json
|
|
11518
|
-
# The hypotenuse daemon's config.
|
|
11745
|
+
# The hypotenuse daemon's config. Only what has no default is set; the rest is read from git and
|
|
11746
|
+
# your sign-in. Every key: https://www.hypotenuse.cloud/docs/config
|
|
11519
11747
|
# Check it with \`hypotenuse config validate\`; apply edits with \`hypotenuse daemon reload\`.
|
|
11520
11748
|
config_version = 1
|
|
11521
11749
|
|
|
@@ -11532,8 +11760,9 @@ roles = ["manager", "worker"] # drop "manager" to only implement issues
|
|
|
11532
11760
|
# teams = ["ENG"] # only these Linear teams (empty: every team)
|
|
11533
11761
|
# labels = ["frontend"] # an issue needs one of these labels
|
|
11534
11762
|
|
|
11535
|
-
|
|
11536
|
-
|
|
11763
|
+
# Only to change how an agent is launched; a role can name a preset directly.
|
|
11764
|
+
# [agents.{{agent}}]
|
|
11765
|
+
# preset = "{{agent}}"
|
|
11537
11766
|
# max_parallel = 2 # jobs using this agent at once
|
|
11538
11767
|
|
|
11539
11768
|
[roles.worker]
|
|
@@ -11556,7 +11785,7 @@ agent = "{{agent}}"
|
|
|
11556
11785
|
# keepalive_interval = "10m" # must stay under Linear's 30m stale window
|
|
11557
11786
|
|
|
11558
11787
|
[log]
|
|
11559
|
-
level = "info"
|
|
11788
|
+
# level = "info"
|
|
11560
11789
|
`;
|
|
11561
11790
|
|
|
11562
11791
|
// src/commands/config.ts
|
|
@@ -11571,41 +11800,31 @@ function detectPreset(machine) {
|
|
|
11571
11800
|
}
|
|
11572
11801
|
return "claude";
|
|
11573
11802
|
}
|
|
11574
|
-
function repositoryBlocks(
|
|
11803
|
+
function repositoryBlocks(paths, env) {
|
|
11804
|
+
const overrides = [
|
|
11805
|
+
'# base_branch = "develop" # default: the branch origin/HEAD names',
|
|
11806
|
+
'# setup = ["bun install --frozen-lockfile"]'
|
|
11807
|
+
];
|
|
11575
11808
|
if (paths.length === 0) {
|
|
11576
11809
|
return [
|
|
11577
|
-
"# One table per
|
|
11810
|
+
"# One table per git clone this runner works in (`config init --repo <path>` fills these in):",
|
|
11578
11811
|
"# [[repositories]]",
|
|
11579
|
-
'# name = "web"',
|
|
11580
11812
|
'# path = "~/code/web"',
|
|
11581
|
-
|
|
11582
|
-
'# base_branch = "main"',
|
|
11583
|
-
'# setup = ["bun install --frozen-lockfile"]'
|
|
11813
|
+
...overrides
|
|
11584
11814
|
].join(`
|
|
11585
11815
|
`);
|
|
11586
11816
|
}
|
|
11587
|
-
return paths.map((path) =>
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
const base = originHead(machine, path) ?? "main";
|
|
11594
|
-
return [
|
|
11595
|
-
"[[repositories]]",
|
|
11596
|
-
`name = ${JSON.stringify(name)}`,
|
|
11597
|
-
`path = ${JSON.stringify(tildify(path, env))}`,
|
|
11598
|
-
remote ? `remote = ${JSON.stringify(remote)}` : "# remote = (no origin remote found)",
|
|
11599
|
-
`base_branch = ${JSON.stringify(base)}`,
|
|
11600
|
-
"# setup = []"
|
|
11601
|
-
].join(`
|
|
11602
|
-
`);
|
|
11603
|
-
}).join(`
|
|
11817
|
+
return paths.map((path) => [
|
|
11818
|
+
"[[repositories]]",
|
|
11819
|
+
`path = ${JSON.stringify(tildify(path, env))}`,
|
|
11820
|
+
...overrides
|
|
11821
|
+
].join(`
|
|
11822
|
+
`)).join(`
|
|
11604
11823
|
|
|
11605
11824
|
`);
|
|
11606
11825
|
}
|
|
11607
11826
|
function renderTemplate2(input) {
|
|
11608
|
-
return template_default.replaceAll("{{server}}",
|
|
11827
|
+
return template_default.replaceAll("{{server}}", `# url = ${JSON.stringify(input.server ?? "https://api.hypotenuse.cloud")} # default: where this machine's runner is registered`).replaceAll("{{hostname}}", hostname5().replace(/\.local$/i, "")).replaceAll("{{jobs}}", String(defaultMaxParallelJobs(input.cpus))).replaceAll("{{agent}}", input.agent).replaceAll("{{repositories}}", input.repositories);
|
|
11609
11828
|
}
|
|
11610
11829
|
async function validateFile(ctx, options) {
|
|
11611
11830
|
const path = ctx.configPath;
|
|
@@ -11666,7 +11885,7 @@ function registerConfigCommands(program) {
|
|
|
11666
11885
|
await runCommand(this, async (ctx) => {
|
|
11667
11886
|
const opts = this.opts();
|
|
11668
11887
|
const path = ctx.configPath;
|
|
11669
|
-
if (
|
|
11888
|
+
if (existsSync19(path) && !opts.force) {
|
|
11670
11889
|
throw new Error(`${path} already exists; pass --force to overwrite it`);
|
|
11671
11890
|
}
|
|
11672
11891
|
const machine = realMachine(ctx.env);
|
|
@@ -11680,10 +11899,10 @@ function registerConfigCommands(program) {
|
|
|
11680
11899
|
const text = renderTemplate2({
|
|
11681
11900
|
server,
|
|
11682
11901
|
agent: detectPreset(machine),
|
|
11683
|
-
repositories: repositoryBlocks(
|
|
11902
|
+
repositories: repositoryBlocks(repos, ctx.env),
|
|
11684
11903
|
cpus: navigator.hardwareConcurrency || 1
|
|
11685
11904
|
});
|
|
11686
|
-
mkdirSync11(
|
|
11905
|
+
mkdirSync11(dirname12(resolve5(path)), { recursive: true, mode: 448 });
|
|
11687
11906
|
writeFileSync6(path, text);
|
|
11688
11907
|
if (!ctx.json)
|
|
11689
11908
|
console.log(`Wrote ${tildify(path, ctx.env)}.`);
|
|
@@ -11724,14 +11943,14 @@ function registerConfigCommands(program) {
|
|
|
11724
11943
|
}
|
|
11725
11944
|
|
|
11726
11945
|
// src/commands/doctor.ts
|
|
11727
|
-
import { existsSync as
|
|
11946
|
+
import { existsSync as existsSync21, statfsSync as statfsSync2, statSync as statSync6 } from "fs";
|
|
11728
11947
|
import { hostname as hostname6 } from "os";
|
|
11729
|
-
import { dirname as
|
|
11948
|
+
import { dirname as dirname14 } from "path";
|
|
11730
11949
|
|
|
11731
11950
|
// src/commands/service.ts
|
|
11732
|
-
import { existsSync as
|
|
11951
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
11733
11952
|
import { homedir as homedir3, userInfo } from "os";
|
|
11734
|
-
import { dirname as
|
|
11953
|
+
import { dirname as dirname13, join as join11 } from "path";
|
|
11735
11954
|
|
|
11736
11955
|
// src/daemon/units/launchd.plist
|
|
11737
11956
|
var launchd_default = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -11867,7 +12086,7 @@ function registerServiceCommands(program) {
|
|
|
11867
12086
|
console.log(text);
|
|
11868
12087
|
return;
|
|
11869
12088
|
}
|
|
11870
|
-
mkdirSync12(
|
|
12089
|
+
mkdirSync12(dirname13(path), { recursive: true });
|
|
11871
12090
|
mkdirSync12(paths.stateDir, { recursive: true, mode: 448 });
|
|
11872
12091
|
writeFileSync7(path, text);
|
|
11873
12092
|
if (platform === "darwin") {
|
|
@@ -11888,7 +12107,7 @@ function registerServiceCommands(program) {
|
|
|
11888
12107
|
service.command("uninstall").description("Stop the service and remove its unit file").action(async function() {
|
|
11889
12108
|
await runCommand(this, async (ctx) => {
|
|
11890
12109
|
const path = unitPath(process.platform, ctx.env);
|
|
11891
|
-
if (!path || !
|
|
12110
|
+
if (!path || !existsSync20(path)) {
|
|
11892
12111
|
emit(ctx, { removed: false }, () => "No service is installed.");
|
|
11893
12112
|
return;
|
|
11894
12113
|
}
|
|
@@ -11906,7 +12125,7 @@ function registerServiceCommands(program) {
|
|
|
11906
12125
|
service.command("status").description("Show whether the service is installed and loaded, and the daemon's status").action(async function() {
|
|
11907
12126
|
await runCommand(this, async (ctx) => {
|
|
11908
12127
|
const path = unitPath(process.platform, ctx.env);
|
|
11909
|
-
const installed = Boolean(path &&
|
|
12128
|
+
const installed = Boolean(path && existsSync20(path));
|
|
11910
12129
|
let manager = "not installed";
|
|
11911
12130
|
if (installed && process.platform === "darwin") {
|
|
11912
12131
|
manager = run(["launchctl", "print", `gui/${userInfo().uid}/${LAUNCHD_LABEL}`], true).includes("state = running") ? "running" : "loaded, not running";
|
|
@@ -11994,7 +12213,7 @@ async function runDoctor(ctx, options) {
|
|
|
11994
12213
|
return list.checks;
|
|
11995
12214
|
}
|
|
11996
12215
|
let runner = null;
|
|
11997
|
-
if (!
|
|
12216
|
+
if (!existsSync21(ctx.credentialsPath)) {
|
|
11998
12217
|
list.fail("credentials", `${tildify(ctx.credentialsPath, ctx.env)} does not exist \u2014 \`hypotenuse login\`, then \`hypotenuse runner register\``);
|
|
11999
12218
|
} else {
|
|
12000
12219
|
try {
|
|
@@ -12057,7 +12276,7 @@ async function runDoctor(ctx, options) {
|
|
|
12057
12276
|
list.warn("daemon", "not running \u2014 `hypotenuse daemon start` or `hypotenuse service install`");
|
|
12058
12277
|
}
|
|
12059
12278
|
const unit = unitPath(process.platform, ctx.env);
|
|
12060
|
-
if (unit &&
|
|
12279
|
+
if (unit && existsSync21(unit))
|
|
12061
12280
|
list.ok("daemon", `service unit ${tildify(unit, ctx.env)}`);
|
|
12062
12281
|
else if (unit)
|
|
12063
12282
|
list.warn("daemon", "no service unit \u2014 `hypotenuse service install` starts the daemon at login");
|
|
@@ -12125,8 +12344,8 @@ async function runDoctor(ctx, options) {
|
|
|
12125
12344
|
`).at(-1) ?? ""}`);
|
|
12126
12345
|
}
|
|
12127
12346
|
let probePath = config.sessions.worktree_root;
|
|
12128
|
-
while (!
|
|
12129
|
-
probePath =
|
|
12347
|
+
while (!existsSync21(probePath) && dirname14(probePath) !== probePath)
|
|
12348
|
+
probePath = dirname14(probePath);
|
|
12130
12349
|
try {
|
|
12131
12350
|
const fs = statfsSync2(probePath);
|
|
12132
12351
|
const free = fs.bavail * fs.bsize;
|
|
@@ -13716,7 +13935,7 @@ function registerSessionCommands(program) {
|
|
|
13716
13935
|
}
|
|
13717
13936
|
|
|
13718
13937
|
// src/commands/skill.ts
|
|
13719
|
-
import { existsSync as
|
|
13938
|
+
import { existsSync as existsSync22, readFileSync as readFileSync13 } from "fs";
|
|
13720
13939
|
import { join as join13 } from "path";
|
|
13721
13940
|
var SKILL_FILE = "hypotenuse.md";
|
|
13722
13941
|
async function readSkill() {
|
|
@@ -13725,8 +13944,8 @@ async function readSkill() {
|
|
|
13725
13944
|
return embedded.text();
|
|
13726
13945
|
for (const up of [[".."], ["..", ".."]]) {
|
|
13727
13946
|
const path = join13(import.meta.dir, ...up, "skills", SKILL_FILE);
|
|
13728
|
-
if (
|
|
13729
|
-
return
|
|
13947
|
+
if (existsSync22(path))
|
|
13948
|
+
return readFileSync13(path, "utf8");
|
|
13730
13949
|
}
|
|
13731
13950
|
throw new Error(`This install has no skills/${SKILL_FILE}; reinstall hypotenuse.`);
|
|
13732
13951
|
}
|
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; `--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. |
|
|
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 — the daemon also watches its config file, reloading on save and restarting itself (after running turns finish) when a change needs it. 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. |
|