@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.
Files changed (2) hide show
  1. package/dist/hypotenuse.js +272 -251
  2. package/package.json +1 -1
@@ -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.17.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 = repo.remote ? normalizeRemote(repo.remote) : originOf(machine, repo.path);
2592
+ const remote = originOf(machine, repo.path);
2596
2593
  if (!remote)
2597
2594
  return null;
2598
2595
  return {
2599
- name: repositoryName({ ...repo, remote }),
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}.remote`,
3261
+ path: `${at}.path`,
3292
3262
  message: `${home(repo.path)} has no origin remote`
3293
3263
  });
3294
3264
  return;
3295
3265
  }
3296
- if (configured && origin !== configured) {
3266
+ const first = origins.get(origin);
3267
+ if (first !== undefined) {
3297
3268
  errors.push({
3298
- path: `${at}.remote`,
3299
- message: `origin is ${origin}, config says ${configured}`
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 isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
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 isAbsolute5(path) ? resolve2(path) : resolve2(root, path);
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 isAbsolute6 } from "path";
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: isAbsolute6(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? 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
- if (!remote)
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 isAbsolute7 } from "path";
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 relative4, resolve as resolve3 } from "path";
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 = resolve3(this.config().sessions.worktree_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 = relative4(root, path);
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 (isAbsolute7(command))
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 resolve4 } from "path";
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. Every key: https://www.hypotenuse.cloud/docs/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
- [agents.{{agent}}]
11536
- preset = "{{agent}}"
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(machine, paths, env) {
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 repository this runner works in (`config init --repo <path>` fills these in):",
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
- '# remote = "github.com/acme/web"',
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
- const remote = originOf(machine, path);
11589
- const name = repositoryName({
11590
- path,
11591
- remote: remote ?? undefined
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}}", input.server ? `url = ${JSON.stringify(input.server)}` : '# url = "https://api.hypotenuse.cloud"').replaceAll("{{hostname}}", hostname5().replace(/\.local$/i, "")).replaceAll("{{jobs}}", String(defaultMaxParallelJobs(input.cpus))).replaceAll("{{agent}}", input.agent).replaceAll("{{repositories}}", input.repositories);
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(machine, repos, ctx.env),
11704
+ repositories: repositoryBlocks(repos, ctx.env),
11684
11705
  cpus: navigator.hardwareConcurrency || 1
11685
11706
  });
11686
- mkdirSync11(dirname11(resolve4(path)), { recursive: true, mode: 448 });
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)}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadeai/hypotenuse",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Hypotenuse — run coding agents on your machine for Linear agent sessions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",