@arcadeai/hypotenuse 0.16.1 → 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 +65 -9
  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.1",
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 [
@@ -6209,6 +6224,26 @@ function fenceFor(text) {
6209
6224
  const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
6210
6225
  return "`".repeat(Math.max(3, longest + 1));
6211
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
+ }
6212
6247
  function reviewSection(review) {
6213
6248
  const issue = review.childIssueIdentifier ?? "the dispatched issue";
6214
6249
  const body = [
@@ -6315,7 +6350,8 @@ ${g.body.trim()}
6315
6350
  `);
6316
6351
  const text = [
6317
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)}`),
6318
- 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 ?? [])),
6319
6355
  section("linear-context", assignment.promptContext),
6320
6356
  section("guidance", guidance),
6321
6357
  section("conversation", conversation(assignment)),
@@ -6908,11 +6944,29 @@ class Turn {
6908
6944
  const conn = this.conn;
6909
6945
  const sessionId = this.acpSessionId;
6910
6946
  let templateText = null;
6911
- if (this.role?.prompt_file) {
6912
- try {
6913
- templateText = readFileSync7(this.role.prompt_file, "utf8");
6914
- } catch (error) {
6915
- 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
+ }
6916
6970
  }
6917
6971
  }
6918
6972
  const blocks = buildPrompt(ctx.assignment, this.role, {
@@ -6922,6 +6976,8 @@ class Turn {
6922
6976
  runnerName: ctx.runnerName,
6923
6977
  resumed: this.resumed,
6924
6978
  templateText,
6979
+ templateUrl,
6980
+ docs,
6925
6981
  readOnly: this.workspace?.mode === "readonly"
6926
6982
  }, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
6927
6983
  this.promptStartedAt = this.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadeai/hypotenuse",
3
- "version": "0.16.1",
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",