@arcadeai/hypotenuse 0.16.0 → 0.17.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 +67 -10
  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.16.0",
346
+ version: "0.17.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",
@@ -2137,6 +2137,9 @@ function expandPath(raw, baseDir, env = process.env) {
2137
2137
  value = value.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_, braced, bare) => env[braced ?? bare] ?? "");
2138
2138
  return isAbsolute(value) ? resolve(value) : resolve(baseDir, value);
2139
2139
  }
2140
+ function isRemoteSource(source) {
2141
+ return /^https?:\/\//i.test(source.trim());
2142
+ }
2140
2143
  function tildify(path, env = process.env) {
2141
2144
  const home = homeOf(env);
2142
2145
  return path === home ? "~" : path.startsWith(`${home}/`) ? `~/${path.slice(home.length + 1)}` : path;
@@ -2201,6 +2204,9 @@ function machineContext(baseDir, env = process.env) {
2201
2204
  }
2202
2205
  function buildConfigSchema(ctx) {
2203
2206
  const path = () => z9.string().trim().min(1).transform((value) => expandPath(value, ctx.baseDir, ctx.env));
2207
+ const source = () => z9.string().trim().min(1).refine((value) => !isRemoteSource(value) || URL.canParse(value), {
2208
+ message: "expected a path or an http(s) URL"
2209
+ }).transform((value) => isRemoteSource(value) ? value : expandPath(value, ctx.baseDir, ctx.env));
2204
2210
  const duration = () => z9.string().regex(DURATION_PATTERN, "expected a duration such as 45s, 10m, 2h, or 7d").transform(parseDuration);
2205
2211
  const size = () => z9.string().regex(SIZE_PATTERN, "expected a size such as 500MB or 2GB").transform(parseSize);
2206
2212
  const parallel = () => z9.number().int().min(1).max(32);
@@ -2223,7 +2229,8 @@ function buildConfigSchema(ctx) {
2223
2229
  const role = (fallbackPolicy) => z9.strictObject({
2224
2230
  agent: z9.string().trim().min(1).describe("An [agents.<id>] id or a preset name"),
2225
2231
  prompt: z9.string().min(1).optional().describe("The role's entry prompt template, inline"),
2226
- prompt_file: path().optional().describe("A Markdown prompt template file"),
2232
+ prompt_file: source().optional().describe("A Markdown prompt template file, or a URL of instructions the agent reads with its own tools"),
2233
+ 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
2234
  permission_policy: permissionPolicy(fallbackPolicy),
2228
2235
  max_parallel: parallel().optional().describe("Jobs of this role at once"),
2229
2236
  mode: z9.string().trim().min(1).optional().describe("The ACP session mode to select"),
@@ -3315,7 +3322,7 @@ function validateMachine(config, machine, options = {}) {
3315
3322
  ]) {
3316
3323
  if (!table)
3317
3324
  continue;
3318
- if (table.prompt_file !== undefined) {
3325
+ if (table.prompt_file !== undefined && !isRemoteSource(table.prompt_file)) {
3319
3326
  const text = machine.readFile(table.prompt_file);
3320
3327
  if (text === null) {
3321
3328
  errors.push({
@@ -3326,6 +3333,14 @@ function validateMachine(config, machine, options = {}) {
3326
3333
  checkTemplate(`roles.${role}.prompt_file`, text, errors);
3327
3334
  }
3328
3335
  }
3336
+ table.docs.forEach((doc, index) => {
3337
+ if (isRemoteSource(doc) || machine.readFile(doc) !== null)
3338
+ return;
3339
+ errors.push({
3340
+ path: `roles.${role}.docs[${index}]`,
3341
+ message: `${home(doc)} cannot be read`
3342
+ });
3343
+ });
3329
3344
  table.extra_mcp_servers.forEach((server, index) => {
3330
3345
  const at = `roles.${role}.extra_mcp_servers[${index}]`;
3331
3346
  for (const [key, name] of [
@@ -4787,7 +4802,8 @@ function runSetupCommand(command, options) {
4787
4802
  const started = now();
4788
4803
  const tail = [];
4789
4804
  return new Promise((resolve) => {
4790
- const child = spawn("sh", ["-c", command], {
4805
+ const child = spawn("sh", ["-c", `exec 2>&1
4806
+ ${command}`], {
4791
4807
  cwd: options.cwd,
4792
4808
  env: options.env,
4793
4809
  stdio: ["ignore", "pipe", "pipe"],
@@ -6208,6 +6224,26 @@ function fenceFor(text) {
6208
6224
  const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
6209
6225
  return "`".repeat(Math.max(3, longest + 1));
6210
6226
  }
6227
+ function remoteInstructions(url) {
6228
+ 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.`;
6229
+ }
6230
+ function roleDocsSection(docs) {
6231
+ if (docs.length === 0)
6232
+ return "";
6233
+ const links = docs.filter((d) => d.text === null).map((d) => d.source);
6234
+ const files = docs.filter((d) => d.text !== null);
6235
+ return [
6236
+ "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.",
6237
+ links.length ? `Read each of these with your tools before you begin. If one cannot be read, say so in your response.
6238
+ ${links.map((l) => `- ${l}`).join(`
6239
+ `)}` : null,
6240
+ ...files.map((d) => `<doc source=${attr(d.source)}>
6241
+ ${(d.text ?? "").trim()}
6242
+ </doc>`)
6243
+ ].filter(Boolean).join(`
6244
+
6245
+ `);
6246
+ }
6211
6247
  function reviewSection(review) {
6212
6248
  const issue = review.childIssueIdentifier ?? "the dispatched issue";
6213
6249
  const body = [
@@ -6314,7 +6350,8 @@ ${g.body.trim()}
6314
6350
  `);
6315
6351
  const text = [
6316
6352
  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)}`),
6317
- section("role-instructions", renderTemplate(template, values, onUnknown)),
6353
+ section("role-instructions", ctx.templateUrl ? remoteInstructions(ctx.templateUrl) : renderTemplate(template, values, onUnknown)),
6354
+ section("role-docs", roleDocsSection(ctx.docs ?? [])),
6318
6355
  section("linear-context", assignment.promptContext),
6319
6356
  section("guidance", guidance),
6320
6357
  section("conversation", conversation(assignment)),
@@ -6907,11 +6944,29 @@ class Turn {
6907
6944
  const conn = this.conn;
6908
6945
  const sessionId = this.acpSessionId;
6909
6946
  let templateText = null;
6910
- if (this.role?.prompt_file) {
6911
- try {
6912
- templateText = readFileSync7(this.role.prompt_file, "utf8");
6913
- } catch (error) {
6914
- throw new TurnFailure(`Runner ${ctx.runnerName} cannot read its ${ctx.assignment.role} prompt file: ${errorMessage(error)}`);
6947
+ let templateUrl = null;
6948
+ const docs = [];
6949
+ if (!this.resumed) {
6950
+ const promptFile = this.role?.prompt_file;
6951
+ if (promptFile && isRemoteSource(promptFile))
6952
+ templateUrl = promptFile;
6953
+ else if (promptFile) {
6954
+ try {
6955
+ templateText = readFileSync7(promptFile, "utf8");
6956
+ } catch (error) {
6957
+ throw new TurnFailure(`Runner ${ctx.runnerName} cannot read its ${ctx.assignment.role} prompt file: ${errorMessage(error)}`);
6958
+ }
6959
+ }
6960
+ for (const doc of this.role?.docs ?? []) {
6961
+ if (isRemoteSource(doc)) {
6962
+ docs.push({ source: doc, text: null });
6963
+ continue;
6964
+ }
6965
+ try {
6966
+ docs.push({ source: tildify(doc), text: readFileSync7(doc, "utf8") });
6967
+ } catch (error) {
6968
+ throw new TurnFailure(`Runner ${ctx.runnerName} cannot read a ${ctx.assignment.role} doc: ${errorMessage(error)}`);
6969
+ }
6915
6970
  }
6916
6971
  }
6917
6972
  const blocks = buildPrompt(ctx.assignment, this.role, {
@@ -6921,6 +6976,8 @@ class Turn {
6921
6976
  runnerName: ctx.runnerName,
6922
6977
  resumed: this.resumed,
6923
6978
  templateText,
6979
+ templateUrl,
6980
+ docs,
6924
6981
  readOnly: this.workspace?.mode === "readonly"
6925
6982
  }, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
6926
6983
  this.promptStartedAt = this.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadeai/hypotenuse",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",