@arcadeai/hypotenuse 0.16.1 → 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 +336 -259
- 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,
|
|
@@ -2137,6 +2135,9 @@ function expandPath(raw, baseDir, env = process.env) {
|
|
|
2137
2135
|
value = value.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_, braced, bare) => env[braced ?? bare] ?? "");
|
|
2138
2136
|
return isAbsolute(value) ? resolve(value) : resolve(baseDir, value);
|
|
2139
2137
|
}
|
|
2138
|
+
function isRemoteSource(source) {
|
|
2139
|
+
return /^https?:\/\//i.test(source.trim());
|
|
2140
|
+
}
|
|
2140
2141
|
function tildify(path, env = process.env) {
|
|
2141
2142
|
const home = homeOf(env);
|
|
2142
2143
|
return path === home ? "~" : path.startsWith(`${home}/`) ? `~/${path.slice(home.length + 1)}` : path;
|
|
@@ -2201,6 +2202,9 @@ function machineContext(baseDir, env = process.env) {
|
|
|
2201
2202
|
}
|
|
2202
2203
|
function buildConfigSchema(ctx) {
|
|
2203
2204
|
const path = () => z9.string().trim().min(1).transform((value) => expandPath(value, ctx.baseDir, ctx.env));
|
|
2205
|
+
const source = () => z9.string().trim().min(1).refine((value) => !isRemoteSource(value) || URL.canParse(value), {
|
|
2206
|
+
message: "expected a path or an http(s) URL"
|
|
2207
|
+
}).transform((value) => isRemoteSource(value) ? value : expandPath(value, ctx.baseDir, ctx.env));
|
|
2204
2208
|
const duration = () => z9.string().regex(DURATION_PATTERN, "expected a duration such as 45s, 10m, 2h, or 7d").transform(parseDuration);
|
|
2205
2209
|
const size = () => z9.string().regex(SIZE_PATTERN, "expected a size such as 500MB or 2GB").transform(parseSize);
|
|
2206
2210
|
const parallel = () => z9.number().int().min(1).max(32);
|
|
@@ -2223,7 +2227,8 @@ function buildConfigSchema(ctx) {
|
|
|
2223
2227
|
const role = (fallbackPolicy) => z9.strictObject({
|
|
2224
2228
|
agent: z9.string().trim().min(1).describe("An [agents.<id>] id or a preset name"),
|
|
2225
2229
|
prompt: z9.string().min(1).optional().describe("The role's entry prompt template, inline"),
|
|
2226
|
-
prompt_file:
|
|
2230
|
+
prompt_file: source().optional().describe("A Markdown prompt template file, or a URL of instructions the agent reads with its own tools"),
|
|
2231
|
+
docs: z9.array(source()).default([]).describe("Documents added to the role's instructions: a file's text is included, a URL is linked for the agent to read with its own tools"),
|
|
2227
2232
|
permission_policy: permissionPolicy(fallbackPolicy),
|
|
2228
2233
|
max_parallel: parallel().optional().describe("Jobs of this role at once"),
|
|
2229
2234
|
mode: z9.string().trim().min(1).optional().describe("The ACP session mode to select"),
|
|
@@ -2245,7 +2250,6 @@ function buildConfigSchema(ctx) {
|
|
|
2245
2250
|
const repository = z9.strictObject({
|
|
2246
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"),
|
|
2247
2252
|
path: path().describe("An existing git clone"),
|
|
2248
|
-
remote: z9.string().trim().min(1).optional().describe("The repository's remote; must match origin"),
|
|
2249
2253
|
base_branch: z9.string().trim().min(1).optional().describe("The branch worktrees start from"),
|
|
2250
2254
|
setup: strings().default([]).describe("Shell commands run in each new worktree"),
|
|
2251
2255
|
setup_timeout: duration().prefault("10m").describe("Per setup command"),
|
|
@@ -2585,11 +2589,11 @@ function originHead(machine, path) {
|
|
|
2585
2589
|
return result.code === 0 && ref.startsWith("origin/") ? ref.slice("origin/".length) : null;
|
|
2586
2590
|
}
|
|
2587
2591
|
function resolveRepository(machine, repo) {
|
|
2588
|
-
const remote =
|
|
2592
|
+
const remote = originOf(machine, repo.path);
|
|
2589
2593
|
if (!remote)
|
|
2590
2594
|
return null;
|
|
2591
2595
|
return {
|
|
2592
|
-
name: repositoryName(
|
|
2596
|
+
name: repositoryName(repo, remote),
|
|
2593
2597
|
remote,
|
|
2594
2598
|
baseBranch: repo.base_branch ?? originHead(machine, repo.path) ?? "main",
|
|
2595
2599
|
path: repo.path
|
|
@@ -3133,35 +3137,7 @@ function validateReferences(config) {
|
|
|
3133
3137
|
hint: 'the default denies edit, delete, move, and execute: { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }'
|
|
3134
3138
|
});
|
|
3135
3139
|
}
|
|
3136
|
-
const names = new Map;
|
|
3137
|
-
const remotes = new Map;
|
|
3138
3140
|
config.repositories.forEach((repo, index) => {
|
|
3139
|
-
const name = repositoryName(repo);
|
|
3140
|
-
if (!repo.name && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
|
|
3141
|
-
errors.push({
|
|
3142
|
-
path: `repositories[${index}]`,
|
|
3143
|
-
message: `"${name}" is not a usable repository name (letters, digits, \`.\`, \`_\`, \`-\`); set name`
|
|
3144
|
-
});
|
|
3145
|
-
}
|
|
3146
|
-
const firstName = names.get(name);
|
|
3147
|
-
if (firstName !== undefined) {
|
|
3148
|
-
errors.push({
|
|
3149
|
-
path: `repositories[${index}].name`,
|
|
3150
|
-
message: `"${name}" is already repositories[${firstName}]`
|
|
3151
|
-
});
|
|
3152
|
-
} else
|
|
3153
|
-
names.set(name, index);
|
|
3154
|
-
if (repo.remote) {
|
|
3155
|
-
const remote = normalizeRemote(repo.remote);
|
|
3156
|
-
const first = remotes.get(remote);
|
|
3157
|
-
if (first !== undefined) {
|
|
3158
|
-
errors.push({
|
|
3159
|
-
path: `repositories[${index}].remote`,
|
|
3160
|
-
message: `${remote} is already repositories[${first}]`
|
|
3161
|
-
});
|
|
3162
|
-
} else
|
|
3163
|
-
remotes.set(remote, index);
|
|
3164
|
-
}
|
|
3165
3141
|
if (repo.cleanup === "ttl" && config.sessions.cleanup_ttl === undefined) {
|
|
3166
3142
|
errors.push({
|
|
3167
3143
|
path: `repositories[${index}].cleanup`,
|
|
@@ -3252,6 +3228,8 @@ function validateMachine(config, machine, options = {}) {
|
|
|
3252
3228
|
message: `git ${version.join(".")} is older than ${MIN_GIT_VERSION.join(".")}`
|
|
3253
3229
|
});
|
|
3254
3230
|
}
|
|
3231
|
+
const origins = new Map;
|
|
3232
|
+
const names = new Map;
|
|
3255
3233
|
config.repositories.forEach((repo, index) => {
|
|
3256
3234
|
const at = `repositories[${index}]`;
|
|
3257
3235
|
if (!machine.isDirectory(repo.path)) {
|
|
@@ -3278,21 +3256,37 @@ function validateMachine(config, machine, options = {}) {
|
|
|
3278
3256
|
return;
|
|
3279
3257
|
}
|
|
3280
3258
|
const origin = originOf(machine, repo.path);
|
|
3281
|
-
const configured = repo.remote ? normalizeRemote(repo.remote) : null;
|
|
3282
3259
|
if (!origin) {
|
|
3283
3260
|
errors.push({
|
|
3284
|
-
path: `${at}.
|
|
3261
|
+
path: `${at}.path`,
|
|
3285
3262
|
message: `${home(repo.path)} has no origin remote`
|
|
3286
3263
|
});
|
|
3287
3264
|
return;
|
|
3288
3265
|
}
|
|
3289
|
-
|
|
3266
|
+
const first = origins.get(origin);
|
|
3267
|
+
if (first !== undefined) {
|
|
3290
3268
|
errors.push({
|
|
3291
|
-
path: `${at}.
|
|
3292
|
-
message:
|
|
3269
|
+
path: `${at}.path`,
|
|
3270
|
+
message: `${home(repo.path)} is another clone of ${origin}, already repositories[${first}]`
|
|
3293
3271
|
});
|
|
3294
3272
|
return;
|
|
3295
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);
|
|
3296
3290
|
const base = repo.base_branch ?? originHead(machine, repo.path) ?? "main";
|
|
3297
3291
|
if (!options.offline) {
|
|
3298
3292
|
const heads = machine.git(["ls-remote", "--heads", "origin", base], repo.path);
|
|
@@ -3315,7 +3309,7 @@ function validateMachine(config, machine, options = {}) {
|
|
|
3315
3309
|
]) {
|
|
3316
3310
|
if (!table)
|
|
3317
3311
|
continue;
|
|
3318
|
-
if (table.prompt_file !== undefined) {
|
|
3312
|
+
if (table.prompt_file !== undefined && !isRemoteSource(table.prompt_file)) {
|
|
3319
3313
|
const text = machine.readFile(table.prompt_file);
|
|
3320
3314
|
if (text === null) {
|
|
3321
3315
|
errors.push({
|
|
@@ -3326,6 +3320,14 @@ function validateMachine(config, machine, options = {}) {
|
|
|
3326
3320
|
checkTemplate(`roles.${role}.prompt_file`, text, errors);
|
|
3327
3321
|
}
|
|
3328
3322
|
}
|
|
3323
|
+
table.docs.forEach((doc, index) => {
|
|
3324
|
+
if (isRemoteSource(doc) || machine.readFile(doc) !== null)
|
|
3325
|
+
return;
|
|
3326
|
+
errors.push({
|
|
3327
|
+
path: `roles.${role}.docs[${index}]`,
|
|
3328
|
+
message: `${home(doc)} cannot be read`
|
|
3329
|
+
});
|
|
3330
|
+
});
|
|
3329
3331
|
table.extra_mcp_servers.forEach((server, index) => {
|
|
3330
3332
|
const at = `roles.${role}.extra_mcp_servers[${index}]`;
|
|
3331
3333
|
for (const [key, name] of [
|
|
@@ -4619,7 +4621,7 @@ function coAuthorEnvironment(env, hooksDir, coAuthor) {
|
|
|
4619
4621
|
|
|
4620
4622
|
// src/worktree/manager.ts
|
|
4621
4623
|
import { existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
4622
|
-
import { join as join6 } from "path";
|
|
4624
|
+
import { isAbsolute as isAbsolute5, join as join6, relative as relative4, resolve as resolve2 } from "path";
|
|
4623
4625
|
|
|
4624
4626
|
// src/worktree/branch.ts
|
|
4625
4627
|
var SLUG_MAX = 40;
|
|
@@ -5172,6 +5174,10 @@ ${stderrTail(fetched)}
|
|
|
5172
5174
|
return { row: previous, reused: true };
|
|
5173
5175
|
}
|
|
5174
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 };
|
|
5175
5181
|
}
|
|
5176
5182
|
if (!this.diskStatus(request.config).enough) {
|
|
5177
5183
|
if (!cleaned)
|
|
@@ -5222,6 +5228,45 @@ ${stderrTail(fetched)}
|
|
|
5222
5228
|
return { row: created, reused: false };
|
|
5223
5229
|
});
|
|
5224
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
|
+
}
|
|
5225
5270
|
reusable(agentSessionId) {
|
|
5226
5271
|
const session = this.state.session(agentSessionId);
|
|
5227
5272
|
const row = session?.worktreeId ? this.state.worktree(session.worktreeId) : null;
|
|
@@ -5567,7 +5612,7 @@ import {
|
|
|
5567
5612
|
rmSync as rmSync4,
|
|
5568
5613
|
writeFileSync as writeFileSync4
|
|
5569
5614
|
} from "fs";
|
|
5570
|
-
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";
|
|
5571
5616
|
|
|
5572
5617
|
class OutsideWorktree extends Error {
|
|
5573
5618
|
path;
|
|
@@ -5626,7 +5671,7 @@ class ConfinedFs {
|
|
|
5626
5671
|
const root = this.roots[0];
|
|
5627
5672
|
if (!root)
|
|
5628
5673
|
throw new OutsideWorktree(path);
|
|
5629
|
-
return
|
|
5674
|
+
return isAbsolute6(path) ? resolve3(path) : resolve3(root, path);
|
|
5630
5675
|
}
|
|
5631
5676
|
read(path, line, limit) {
|
|
5632
5677
|
const real = this.resolveExisting(path);
|
|
@@ -5920,7 +5965,7 @@ function callText(call) {
|
|
|
5920
5965
|
}
|
|
5921
5966
|
|
|
5922
5967
|
// src/acp/mcp-servers.ts
|
|
5923
|
-
import { isAbsolute as
|
|
5968
|
+
import { isAbsolute as isAbsolute7 } from "path";
|
|
5924
5969
|
var HYPOTENUSE_SERVER_NAME = "hypotenuse";
|
|
5925
5970
|
var VARIABLE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
5926
5971
|
|
|
@@ -5970,7 +6015,7 @@ function turnMcpServers(role, caps, env, hypotenuse) {
|
|
|
5970
6015
|
const [command, ...args] = s.command;
|
|
5971
6016
|
servers.push({
|
|
5972
6017
|
name: s.name,
|
|
5973
|
-
command:
|
|
6018
|
+
command: isAbsolute7(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
|
|
5974
6019
|
args,
|
|
5975
6020
|
env: pairs(s.name, s.env, env)
|
|
5976
6021
|
});
|
|
@@ -6209,6 +6254,26 @@ function fenceFor(text) {
|
|
|
6209
6254
|
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
6210
6255
|
return "`".repeat(Math.max(3, longest + 1));
|
|
6211
6256
|
}
|
|
6257
|
+
function remoteInstructions(url) {
|
|
6258
|
+
return `Your instructions for this role are the document at ${url}. Read it with your tools before doing anything else, and follow it. If you cannot read it, say so in your response and stop.`;
|
|
6259
|
+
}
|
|
6260
|
+
function roleDocsSection(docs) {
|
|
6261
|
+
if (docs.length === 0)
|
|
6262
|
+
return "";
|
|
6263
|
+
const links = docs.filter((d) => d.text === null).map((d) => d.source);
|
|
6264
|
+
const files = docs.filter((d) => d.text !== null);
|
|
6265
|
+
return [
|
|
6266
|
+
"These documents are part of your instructions: they say how this team works. Where one disagrees with the instructions above about your tools or what you never do, the instructions above win.",
|
|
6267
|
+
links.length ? `Read each of these with your tools before you begin. If one cannot be read, say so in your response.
|
|
6268
|
+
${links.map((l) => `- ${l}`).join(`
|
|
6269
|
+
`)}` : null,
|
|
6270
|
+
...files.map((d) => `<doc source=${attr(d.source)}>
|
|
6271
|
+
${(d.text ?? "").trim()}
|
|
6272
|
+
</doc>`)
|
|
6273
|
+
].filter(Boolean).join(`
|
|
6274
|
+
|
|
6275
|
+
`);
|
|
6276
|
+
}
|
|
6212
6277
|
function reviewSection(review) {
|
|
6213
6278
|
const issue = review.childIssueIdentifier ?? "the dispatched issue";
|
|
6214
6279
|
const body = [
|
|
@@ -6315,7 +6380,8 @@ ${g.body.trim()}
|
|
|
6315
6380
|
`);
|
|
6316
6381
|
const text = [
|
|
6317
6382
|
section("hypotenuse", `You are working as the ${assignment.role} agent for ${who}${creator ? ` on behalf of ${creator}` : ""}, in a Linear agent session. You are an AI agent; never claim to be a person. What you say at the end of your turn is posted to the session as your response.${dispatchedBy(assignment)}`),
|
|
6318
|
-
section("role-instructions", renderTemplate(template, values, onUnknown)),
|
|
6383
|
+
section("role-instructions", ctx.templateUrl ? remoteInstructions(ctx.templateUrl) : renderTemplate(template, values, onUnknown)),
|
|
6384
|
+
section("role-docs", roleDocsSection(ctx.docs ?? [])),
|
|
6319
6385
|
section("linear-context", assignment.promptContext),
|
|
6320
6386
|
section("guidance", guidance),
|
|
6321
6387
|
section("conversation", conversation(assignment)),
|
|
@@ -6908,11 +6974,29 @@ class Turn {
|
|
|
6908
6974
|
const conn = this.conn;
|
|
6909
6975
|
const sessionId = this.acpSessionId;
|
|
6910
6976
|
let templateText = null;
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6977
|
+
let templateUrl = null;
|
|
6978
|
+
const docs = [];
|
|
6979
|
+
if (!this.resumed) {
|
|
6980
|
+
const promptFile = this.role?.prompt_file;
|
|
6981
|
+
if (promptFile && isRemoteSource(promptFile))
|
|
6982
|
+
templateUrl = promptFile;
|
|
6983
|
+
else if (promptFile) {
|
|
6984
|
+
try {
|
|
6985
|
+
templateText = readFileSync7(promptFile, "utf8");
|
|
6986
|
+
} catch (error) {
|
|
6987
|
+
throw new TurnFailure(`Runner ${ctx.runnerName} cannot read its ${ctx.assignment.role} prompt file: ${errorMessage(error)}`);
|
|
6988
|
+
}
|
|
6989
|
+
}
|
|
6990
|
+
for (const doc of this.role?.docs ?? []) {
|
|
6991
|
+
if (isRemoteSource(doc)) {
|
|
6992
|
+
docs.push({ source: doc, text: null });
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6995
|
+
try {
|
|
6996
|
+
docs.push({ source: tildify(doc), text: readFileSync7(doc, "utf8") });
|
|
6997
|
+
} catch (error) {
|
|
6998
|
+
throw new TurnFailure(`Runner ${ctx.runnerName} cannot read a ${ctx.assignment.role} doc: ${errorMessage(error)}`);
|
|
6999
|
+
}
|
|
6916
7000
|
}
|
|
6917
7001
|
}
|
|
6918
7002
|
const blocks = buildPrompt(ctx.assignment, this.role, {
|
|
@@ -6922,6 +7006,8 @@ class Turn {
|
|
|
6922
7006
|
runnerName: ctx.runnerName,
|
|
6923
7007
|
resumed: this.resumed,
|
|
6924
7008
|
templateText,
|
|
7009
|
+
templateUrl,
|
|
7010
|
+
docs,
|
|
6925
7011
|
readOnly: this.workspace?.mode === "readonly"
|
|
6926
7012
|
}, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
|
|
6927
7013
|
this.promptStartedAt = this.now();
|
|
@@ -6976,11 +7062,10 @@ class Turn {
|
|
|
6976
7062
|
return stop === "end_turn" ? this.result("completed") : this.result("failed", `stopReason: ${stop}`);
|
|
6977
7063
|
}
|
|
6978
7064
|
repositoryName() {
|
|
7065
|
+
if (this.workspace?.repository)
|
|
7066
|
+
return this.workspace.repository.name;
|
|
6979
7067
|
const remote = this.ctx.assignment.repository?.remote;
|
|
6980
|
-
|
|
6981
|
-
return null;
|
|
6982
|
-
const repo = this.ctx.config.repositories.find((r) => r.remote && r.remote.includes(remote));
|
|
6983
|
-
return repo ? repositoryName(repo) : remote.split("/").pop() ?? null;
|
|
7068
|
+
return remote ? remote.split("/").pop() ?? null : null;
|
|
6984
7069
|
}
|
|
6985
7070
|
async stopped() {
|
|
6986
7071
|
const reason = this.endReason ?? "stop";
|
|
@@ -7946,7 +8031,7 @@ class StateDb {
|
|
|
7946
8031
|
|
|
7947
8032
|
// src/daemon/supervisor.ts
|
|
7948
8033
|
import { hostname as hostname3 } from "os";
|
|
7949
|
-
import { isAbsolute as
|
|
8034
|
+
import { isAbsolute as isAbsolute8 } from "path";
|
|
7950
8035
|
|
|
7951
8036
|
// src/telemetry/collector.ts
|
|
7952
8037
|
var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
|
|
@@ -8514,7 +8599,7 @@ function resolvedConfigSecrets(config, env) {
|
|
|
8514
8599
|
|
|
8515
8600
|
// src/worktree/cleanup.ts
|
|
8516
8601
|
import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
|
|
8517
|
-
import { join as join9, relative as
|
|
8602
|
+
import { join as join9, relative as relative5, resolve as resolve4 } from "path";
|
|
8518
8603
|
|
|
8519
8604
|
// src/worktree/merge-state.ts
|
|
8520
8605
|
var MERGE_STATE_TTL_MS = 10 * 60000;
|
|
@@ -8677,7 +8762,7 @@ class WorktreeCleaner {
|
|
|
8677
8762
|
return result.stdout.trim() !== "";
|
|
8678
8763
|
}
|
|
8679
8764
|
async adopt() {
|
|
8680
|
-
const root =
|
|
8765
|
+
const root = resolve4(this.config().sessions.worktree_root);
|
|
8681
8766
|
const known = new Set(this.state.worktrees({ live: true }).map((w) => w.path));
|
|
8682
8767
|
let adopted = 0;
|
|
8683
8768
|
for (const repo of this.options.repositories?.() ?? []) {
|
|
@@ -8693,7 +8778,7 @@ class WorktreeCleaner {
|
|
|
8693
8778
|
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
8694
8779
|
if (!path || !branch || known.has(path))
|
|
8695
8780
|
continue;
|
|
8696
|
-
const rel =
|
|
8781
|
+
const rel = relative5(root, path);
|
|
8697
8782
|
if (rel.startsWith("..") || rel === "")
|
|
8698
8783
|
continue;
|
|
8699
8784
|
const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
|
|
@@ -10452,7 +10537,7 @@ function changedKeys(a, b, prefix = "") {
|
|
|
10452
10537
|
return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
|
|
10453
10538
|
}
|
|
10454
10539
|
function resolveCommand(command, env) {
|
|
10455
|
-
if (
|
|
10540
|
+
if (isAbsolute8(command))
|
|
10456
10541
|
return command;
|
|
10457
10542
|
return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
|
|
10458
10543
|
}
|
|
@@ -11455,11 +11540,12 @@ function registerCheckUpdateCommand(program) {
|
|
|
11455
11540
|
// src/commands/config.ts
|
|
11456
11541
|
import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
11457
11542
|
import { hostname as hostname5 } from "os";
|
|
11458
|
-
import { dirname as dirname11, resolve as
|
|
11543
|
+
import { dirname as dirname11, resolve as resolve5 } from "path";
|
|
11459
11544
|
|
|
11460
11545
|
// src/config/template.toml
|
|
11461
11546
|
var template_default = `#:schema https://www.hypotenuse.cloud/schemas/config-v1.json
|
|
11462
|
-
# 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
|
|
11463
11549
|
# Check it with \`hypotenuse config validate\`; apply edits with \`hypotenuse daemon reload\`.
|
|
11464
11550
|
config_version = 1
|
|
11465
11551
|
|
|
@@ -11476,8 +11562,9 @@ roles = ["manager", "worker"] # drop "manager" to only implement issues
|
|
|
11476
11562
|
# teams = ["ENG"] # only these Linear teams (empty: every team)
|
|
11477
11563
|
# labels = ["frontend"] # an issue needs one of these labels
|
|
11478
11564
|
|
|
11479
|
-
|
|
11480
|
-
|
|
11565
|
+
# Only to change how an agent is launched; a role can name a preset directly.
|
|
11566
|
+
# [agents.{{agent}}]
|
|
11567
|
+
# preset = "{{agent}}"
|
|
11481
11568
|
# max_parallel = 2 # jobs using this agent at once
|
|
11482
11569
|
|
|
11483
11570
|
[roles.worker]
|
|
@@ -11500,7 +11587,7 @@ agent = "{{agent}}"
|
|
|
11500
11587
|
# keepalive_interval = "10m" # must stay under Linear's 30m stale window
|
|
11501
11588
|
|
|
11502
11589
|
[log]
|
|
11503
|
-
level = "info"
|
|
11590
|
+
# level = "info"
|
|
11504
11591
|
`;
|
|
11505
11592
|
|
|
11506
11593
|
// src/commands/config.ts
|
|
@@ -11515,41 +11602,31 @@ function detectPreset(machine) {
|
|
|
11515
11602
|
}
|
|
11516
11603
|
return "claude";
|
|
11517
11604
|
}
|
|
11518
|
-
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
|
+
];
|
|
11519
11610
|
if (paths.length === 0) {
|
|
11520
11611
|
return [
|
|
11521
|
-
"# One table per
|
|
11612
|
+
"# One table per git clone this runner works in (`config init --repo <path>` fills these in):",
|
|
11522
11613
|
"# [[repositories]]",
|
|
11523
|
-
'# name = "web"',
|
|
11524
11614
|
'# path = "~/code/web"',
|
|
11525
|
-
|
|
11526
|
-
'# base_branch = "main"',
|
|
11527
|
-
'# setup = ["bun install --frozen-lockfile"]'
|
|
11615
|
+
...overrides
|
|
11528
11616
|
].join(`
|
|
11529
11617
|
`);
|
|
11530
11618
|
}
|
|
11531
|
-
return paths.map((path) =>
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
const base = originHead(machine, path) ?? "main";
|
|
11538
|
-
return [
|
|
11539
|
-
"[[repositories]]",
|
|
11540
|
-
`name = ${JSON.stringify(name)}`,
|
|
11541
|
-
`path = ${JSON.stringify(tildify(path, env))}`,
|
|
11542
|
-
remote ? `remote = ${JSON.stringify(remote)}` : "# remote = (no origin remote found)",
|
|
11543
|
-
`base_branch = ${JSON.stringify(base)}`,
|
|
11544
|
-
"# setup = []"
|
|
11545
|
-
].join(`
|
|
11546
|
-
`);
|
|
11547
|
-
}).join(`
|
|
11619
|
+
return paths.map((path) => [
|
|
11620
|
+
"[[repositories]]",
|
|
11621
|
+
`path = ${JSON.stringify(tildify(path, env))}`,
|
|
11622
|
+
...overrides
|
|
11623
|
+
].join(`
|
|
11624
|
+
`)).join(`
|
|
11548
11625
|
|
|
11549
11626
|
`);
|
|
11550
11627
|
}
|
|
11551
11628
|
function renderTemplate2(input) {
|
|
11552
|
-
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);
|
|
11553
11630
|
}
|
|
11554
11631
|
async function validateFile(ctx, options) {
|
|
11555
11632
|
const path = ctx.configPath;
|
|
@@ -11624,10 +11701,10 @@ function registerConfigCommands(program) {
|
|
|
11624
11701
|
const text = renderTemplate2({
|
|
11625
11702
|
server,
|
|
11626
11703
|
agent: detectPreset(machine),
|
|
11627
|
-
repositories: repositoryBlocks(
|
|
11704
|
+
repositories: repositoryBlocks(repos, ctx.env),
|
|
11628
11705
|
cpus: navigator.hardwareConcurrency || 1
|
|
11629
11706
|
});
|
|
11630
|
-
mkdirSync11(dirname11(
|
|
11707
|
+
mkdirSync11(dirname11(resolve5(path)), { recursive: true, mode: 448 });
|
|
11631
11708
|
writeFileSync6(path, text);
|
|
11632
11709
|
if (!ctx.json)
|
|
11633
11710
|
console.log(`Wrote ${tildify(path, ctx.env)}.`);
|