@arcadeai/hypotenuse 0.17.0 → 0.19.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 +272 -251
- package/package.json +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.19.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);
|
|
@@ -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";
|
|
@@ -8002,7 +8031,7 @@ class StateDb {
|
|
|
8002
8031
|
|
|
8003
8032
|
// src/daemon/supervisor.ts
|
|
8004
8033
|
import { hostname as hostname3 } from "os";
|
|
8005
|
-
import { isAbsolute as
|
|
8034
|
+
import { isAbsolute as isAbsolute8 } from "path";
|
|
8006
8035
|
|
|
8007
8036
|
// src/telemetry/collector.ts
|
|
8008
8037
|
var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
|
|
@@ -8570,7 +8599,7 @@ function resolvedConfigSecrets(config, env) {
|
|
|
8570
8599
|
|
|
8571
8600
|
// src/worktree/cleanup.ts
|
|
8572
8601
|
import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
|
|
8573
|
-
import { join as join9, relative as
|
|
8602
|
+
import { join as join9, relative as relative5, resolve as resolve4 } from "path";
|
|
8574
8603
|
|
|
8575
8604
|
// src/worktree/merge-state.ts
|
|
8576
8605
|
var MERGE_STATE_TTL_MS = 10 * 60000;
|
|
@@ -8733,7 +8762,7 @@ class WorktreeCleaner {
|
|
|
8733
8762
|
return result.stdout.trim() !== "";
|
|
8734
8763
|
}
|
|
8735
8764
|
async adopt() {
|
|
8736
|
-
const root =
|
|
8765
|
+
const root = resolve4(this.config().sessions.worktree_root);
|
|
8737
8766
|
const known = new Set(this.state.worktrees({ live: true }).map((w) => w.path));
|
|
8738
8767
|
let adopted = 0;
|
|
8739
8768
|
for (const repo of this.options.repositories?.() ?? []) {
|
|
@@ -8749,7 +8778,7 @@ class WorktreeCleaner {
|
|
|
8749
8778
|
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
8750
8779
|
if (!path || !branch || known.has(path))
|
|
8751
8780
|
continue;
|
|
8752
|
-
const rel =
|
|
8781
|
+
const rel = relative5(root, path);
|
|
8753
8782
|
if (rel.startsWith("..") || rel === "")
|
|
8754
8783
|
continue;
|
|
8755
8784
|
const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
|
|
@@ -10508,7 +10537,7 @@ function changedKeys(a, b, prefix = "") {
|
|
|
10508
10537
|
return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
|
|
10509
10538
|
}
|
|
10510
10539
|
function resolveCommand(command, env) {
|
|
10511
|
-
if (
|
|
10540
|
+
if (isAbsolute8(command))
|
|
10512
10541
|
return command;
|
|
10513
10542
|
return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
|
|
10514
10543
|
}
|
|
@@ -11511,11 +11540,12 @@ function registerCheckUpdateCommand(program) {
|
|
|
11511
11540
|
// src/commands/config.ts
|
|
11512
11541
|
import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
11513
11542
|
import { hostname as hostname5 } from "os";
|
|
11514
|
-
import { dirname as dirname11, resolve as
|
|
11543
|
+
import { dirname as dirname11, resolve as resolve5 } from "path";
|
|
11515
11544
|
|
|
11516
11545
|
// src/config/template.toml
|
|
11517
11546
|
var template_default = `#:schema https://www.hypotenuse.cloud/schemas/config-v1.json
|
|
11518
|
-
# The hypotenuse daemon's config.
|
|
11547
|
+
# The hypotenuse daemon's config. Only what has no default is set; the rest is read from git and
|
|
11548
|
+
# your sign-in. Every key: https://www.hypotenuse.cloud/docs/config
|
|
11519
11549
|
# Check it with \`hypotenuse config validate\`; apply edits with \`hypotenuse daemon reload\`.
|
|
11520
11550
|
config_version = 1
|
|
11521
11551
|
|
|
@@ -11532,8 +11562,9 @@ roles = ["manager", "worker"] # drop "manager" to only implement issues
|
|
|
11532
11562
|
# teams = ["ENG"] # only these Linear teams (empty: every team)
|
|
11533
11563
|
# labels = ["frontend"] # an issue needs one of these labels
|
|
11534
11564
|
|
|
11535
|
-
|
|
11536
|
-
|
|
11565
|
+
# Only to change how an agent is launched; a role can name a preset directly.
|
|
11566
|
+
# [agents.{{agent}}]
|
|
11567
|
+
# preset = "{{agent}}"
|
|
11537
11568
|
# max_parallel = 2 # jobs using this agent at once
|
|
11538
11569
|
|
|
11539
11570
|
[roles.worker]
|
|
@@ -11556,7 +11587,7 @@ agent = "{{agent}}"
|
|
|
11556
11587
|
# keepalive_interval = "10m" # must stay under Linear's 30m stale window
|
|
11557
11588
|
|
|
11558
11589
|
[log]
|
|
11559
|
-
level = "info"
|
|
11590
|
+
# level = "info"
|
|
11560
11591
|
`;
|
|
11561
11592
|
|
|
11562
11593
|
// src/commands/config.ts
|
|
@@ -11571,41 +11602,31 @@ function detectPreset(machine) {
|
|
|
11571
11602
|
}
|
|
11572
11603
|
return "claude";
|
|
11573
11604
|
}
|
|
11574
|
-
function repositoryBlocks(
|
|
11605
|
+
function repositoryBlocks(paths, env) {
|
|
11606
|
+
const overrides = [
|
|
11607
|
+
'# base_branch = "develop" # default: the branch origin/HEAD names',
|
|
11608
|
+
'# setup = ["bun install --frozen-lockfile"]'
|
|
11609
|
+
];
|
|
11575
11610
|
if (paths.length === 0) {
|
|
11576
11611
|
return [
|
|
11577
|
-
"# One table per
|
|
11612
|
+
"# One table per git clone this runner works in (`config init --repo <path>` fills these in):",
|
|
11578
11613
|
"# [[repositories]]",
|
|
11579
|
-
'# name = "web"',
|
|
11580
11614
|
'# path = "~/code/web"',
|
|
11581
|
-
|
|
11582
|
-
'# base_branch = "main"',
|
|
11583
|
-
'# setup = ["bun install --frozen-lockfile"]'
|
|
11615
|
+
...overrides
|
|
11584
11616
|
].join(`
|
|
11585
11617
|
`);
|
|
11586
11618
|
}
|
|
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(`
|
|
11619
|
+
return paths.map((path) => [
|
|
11620
|
+
"[[repositories]]",
|
|
11621
|
+
`path = ${JSON.stringify(tildify(path, env))}`,
|
|
11622
|
+
...overrides
|
|
11623
|
+
].join(`
|
|
11624
|
+
`)).join(`
|
|
11604
11625
|
|
|
11605
11626
|
`);
|
|
11606
11627
|
}
|
|
11607
11628
|
function renderTemplate2(input) {
|
|
11608
|
-
return template_default.replaceAll("{{server}}",
|
|
11629
|
+
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
11630
|
}
|
|
11610
11631
|
async function validateFile(ctx, options) {
|
|
11611
11632
|
const path = ctx.configPath;
|
|
@@ -11680,10 +11701,10 @@ function registerConfigCommands(program) {
|
|
|
11680
11701
|
const text = renderTemplate2({
|
|
11681
11702
|
server,
|
|
11682
11703
|
agent: detectPreset(machine),
|
|
11683
|
-
repositories: repositoryBlocks(
|
|
11704
|
+
repositories: repositoryBlocks(repos, ctx.env),
|
|
11684
11705
|
cpus: navigator.hardwareConcurrency || 1
|
|
11685
11706
|
});
|
|
11686
|
-
mkdirSync11(dirname11(
|
|
11707
|
+
mkdirSync11(dirname11(resolve5(path)), { recursive: true, mode: 448 });
|
|
11687
11708
|
writeFileSync6(path, text);
|
|
11688
11709
|
if (!ctx.json)
|
|
11689
11710
|
console.log(`Wrote ${tildify(path, ctx.env)}.`);
|