@arcadeai/hypotenuse 0.12.0 → 0.13.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 +214 -32
- 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.13.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",
|
|
@@ -852,14 +852,28 @@ var CANCEL_REASONS = [
|
|
|
852
852
|
"dashboard",
|
|
853
853
|
"delegate_removed",
|
|
854
854
|
"issue_closed",
|
|
855
|
+
"unassigned",
|
|
855
856
|
"integration_revoked",
|
|
856
857
|
"superseded"
|
|
857
858
|
];
|
|
858
859
|
var Cancel = z3.looseObject({
|
|
859
860
|
workItemId: z3.number().int().positive(),
|
|
860
861
|
reason: z3.union([z3.enum(CANCEL_REASONS), z3.string().min(1).max(64)]),
|
|
861
|
-
by: z3.string().min(1).max(200).optional()
|
|
862
|
+
by: z3.string().min(1).max(200).optional(),
|
|
863
|
+
said: z3.string().min(1).max(2000).optional()
|
|
862
864
|
});
|
|
865
|
+
var STOP_QUOTE_MAX = 500;
|
|
866
|
+
function stopQuote(said) {
|
|
867
|
+
const text = said?.trim();
|
|
868
|
+
if (!text || /^stop[.!]*$/i.test(text))
|
|
869
|
+
return "";
|
|
870
|
+
const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
|
|
871
|
+
return `
|
|
872
|
+
|
|
873
|
+
> You said: ${cut.split(`
|
|
874
|
+
`).join(`
|
|
875
|
+
> `)}`;
|
|
876
|
+
}
|
|
863
877
|
var Answer = z3.looseObject({
|
|
864
878
|
workItemId: z3.number().int().positive(),
|
|
865
879
|
activityId: z3.number().int().positive(),
|
|
@@ -912,10 +926,14 @@ var SelectMetadata = z5.strictObject({
|
|
|
912
926
|
value: z5.string().trim().min(1).max(1024)
|
|
913
927
|
})).min(1).max(25)
|
|
914
928
|
});
|
|
929
|
+
var AuthProvider = z5.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
|
|
915
930
|
var AuthMetadata = z5.strictObject({
|
|
916
|
-
url: z5.url(),
|
|
931
|
+
url: z5.url().optional(),
|
|
932
|
+
provider: AuthProvider.optional(),
|
|
917
933
|
userId: z5.string().max(255).optional(),
|
|
918
934
|
providerName: z5.string().max(255).optional()
|
|
935
|
+
}).refine((m) => m.url !== undefined || m.provider !== undefined, {
|
|
936
|
+
message: "an auth elicitation needs a url or a provider"
|
|
919
937
|
});
|
|
920
938
|
var ThoughtEvent = z5.strictObject({
|
|
921
939
|
...envelope,
|
|
@@ -3524,7 +3542,7 @@ function stopActivity(ctx) {
|
|
|
3524
3542
|
case "stop":
|
|
3525
3543
|
return {
|
|
3526
3544
|
type: "response",
|
|
3527
|
-
body: `Stopped at your request. Changes so far are ${where}
|
|
3545
|
+
body: `Stopped at your request. Changes so far are ${where}.${stopQuote(ctx.note)}`
|
|
3528
3546
|
};
|
|
3529
3547
|
case "kill":
|
|
3530
3548
|
return {
|
|
@@ -4451,7 +4469,7 @@ function formFields(request) {
|
|
|
4451
4469
|
};
|
|
4452
4470
|
});
|
|
4453
4471
|
}
|
|
4454
|
-
function elicitationQuestion(request, agentTitle) {
|
|
4472
|
+
function elicitationQuestion(request, agentTitle, provider = "agent") {
|
|
4455
4473
|
const message = truncate(request.message.trim() || "The agent has a question.", 60000);
|
|
4456
4474
|
const cancelled = () => ({ action: "cancel" });
|
|
4457
4475
|
const declined = () => ({ action: "decline" });
|
|
@@ -4468,7 +4486,11 @@ function elicitationQuestion(request, agentTitle) {
|
|
|
4468
4486
|
type: "elicitation",
|
|
4469
4487
|
body: message,
|
|
4470
4488
|
signal: "auth",
|
|
4471
|
-
signalMetadata: {
|
|
4489
|
+
signalMetadata: {
|
|
4490
|
+
url,
|
|
4491
|
+
provider,
|
|
4492
|
+
providerName: truncate(agentTitle, 255)
|
|
4493
|
+
}
|
|
4472
4494
|
} : { type: "elicitation", body: `${message}
|
|
4473
4495
|
|
|
4474
4496
|
${url}` },
|
|
@@ -4705,6 +4727,14 @@ var SILENT_KINDS = new Set([
|
|
|
4705
4727
|
"compaction_update",
|
|
4706
4728
|
"compaction_summary_chunk"
|
|
4707
4729
|
]);
|
|
4730
|
+
function responseSignature(who) {
|
|
4731
|
+
const version = who.agentVersion ? ` ${who.agentVersion}` : "";
|
|
4732
|
+
return `
|
|
4733
|
+
|
|
4734
|
+
+++ \u2014 Hypotenuse (${who.role}, ${who.agentTitle})
|
|
4735
|
+
An AI coding agent: \`${who.agentId}\`${version} on runner \`${who.runnerName}\`, delegated through Hypotenuse.
|
|
4736
|
+
+++`;
|
|
4737
|
+
}
|
|
4708
4738
|
|
|
4709
4739
|
class TurnMapper {
|
|
4710
4740
|
options;
|
|
@@ -4868,9 +4898,10 @@ class TurnMapper {
|
|
|
4868
4898
|
switch (stopReason) {
|
|
4869
4899
|
case "end_turn": {
|
|
4870
4900
|
const last = this.lastActionSummary;
|
|
4901
|
+
const signature = context.signature ?? "";
|
|
4871
4902
|
events.push({
|
|
4872
4903
|
type: "response",
|
|
4873
|
-
body: truncate(said || (last ? `Done. Last step: ${last}.` : "Done."), 60000)
|
|
4904
|
+
body: `${truncate(said || (last ? `Done. Last step: ${last}.` : "Done."), 60000 - signature.length)}${signature}`
|
|
4874
4905
|
});
|
|
4875
4906
|
break;
|
|
4876
4907
|
}
|
|
@@ -5079,26 +5110,101 @@ class PrDetector {
|
|
|
5079
5110
|
}
|
|
5080
5111
|
}
|
|
5081
5112
|
|
|
5113
|
+
// src/acp/preflight.ts
|
|
5114
|
+
function providerSlug(agentId) {
|
|
5115
|
+
const slug = agentId.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, 64);
|
|
5116
|
+
return slug || "agent";
|
|
5117
|
+
}
|
|
5118
|
+
var PREFLIGHT_TIMEOUT_MS = 20000;
|
|
5119
|
+
var AUTH_FAILURE = /authentication failed|could not read username|could not read password|permission denied \(publickey|invalid username or password|terminal prompts disabled|support for password authentication was removed|http basic: access denied|403 forbidden|returned error: 40[13]/i;
|
|
5120
|
+
function isGitAuthFailure(output) {
|
|
5121
|
+
return AUTH_FAILURE.test(output);
|
|
5122
|
+
}
|
|
5123
|
+
function remoteHost(remote) {
|
|
5124
|
+
try {
|
|
5125
|
+
const url = new URL(remote);
|
|
5126
|
+
if (url.protocol === "file:")
|
|
5127
|
+
return null;
|
|
5128
|
+
return url.hostname.toLowerCase() || null;
|
|
5129
|
+
} catch {
|
|
5130
|
+
const scp = remote.match(/^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)/);
|
|
5131
|
+
return scp ? scp[1].toLowerCase() : null;
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
function remoteProvider(remote) {
|
|
5135
|
+
const host = remoteHost(remote) ?? "git";
|
|
5136
|
+
if (host === "github.com")
|
|
5137
|
+
return { provider: "github", providerName: "GitHub" };
|
|
5138
|
+
return { provider: host, providerName: host };
|
|
5139
|
+
}
|
|
5140
|
+
var runCommand2 = async (argv, options) => {
|
|
5141
|
+
try {
|
|
5142
|
+
const proc = Bun.spawn(argv, {
|
|
5143
|
+
cwd: options.cwd,
|
|
5144
|
+
env: {
|
|
5145
|
+
...options.env,
|
|
5146
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
5147
|
+
GH_PROMPT_DISABLED: "1"
|
|
5148
|
+
},
|
|
5149
|
+
stdin: "ignore",
|
|
5150
|
+
stdout: "pipe",
|
|
5151
|
+
stderr: "pipe"
|
|
5152
|
+
});
|
|
5153
|
+
const timer = setTimeout(() => proc.kill(), options.timeoutMs);
|
|
5154
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
5155
|
+
new Response(proc.stdout).text(),
|
|
5156
|
+
new Response(proc.stderr).text(),
|
|
5157
|
+
proc.exited
|
|
5158
|
+
]).finally(() => clearTimeout(timer));
|
|
5159
|
+
return { code, stdout, stderr };
|
|
5160
|
+
} catch (error) {
|
|
5161
|
+
return {
|
|
5162
|
+
code: null,
|
|
5163
|
+
stdout: "",
|
|
5164
|
+
stderr: error instanceof Error ? error.message : String(error)
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
};
|
|
5168
|
+
async function checkRepositoryAuth(input, run = runCommand2) {
|
|
5169
|
+
const { remote, cwd, env, runnerName } = input;
|
|
5170
|
+
if (!remote || remoteHost(remote) === null)
|
|
5171
|
+
return null;
|
|
5172
|
+
const provider = remoteProvider(remote);
|
|
5173
|
+
const listed = await run(["git", "ls-remote", "--heads", remote], {
|
|
5174
|
+
cwd,
|
|
5175
|
+
env,
|
|
5176
|
+
timeoutMs: PREFLIGHT_TIMEOUT_MS
|
|
5177
|
+
});
|
|
5178
|
+
if (listed.code !== 0 && isGitAuthFailure(`${listed.stderr}
|
|
5179
|
+
${listed.stdout}`)) {
|
|
5180
|
+
return {
|
|
5181
|
+
...provider,
|
|
5182
|
+
body: `Runner ${runnerName} can't sign in to ${provider.providerName} to reach \`${remote}\`, so it could fetch and push nothing. Its owner can fix it on that machine \u2014 the link says how. Reply here once it's done and I'll carry on.`
|
|
5183
|
+
};
|
|
5184
|
+
}
|
|
5185
|
+
if (provider.provider !== "github" || !input.pullRequests)
|
|
5186
|
+
return null;
|
|
5187
|
+
const gh = await run(["gh", "auth", "status", "--hostname", "github.com"], {
|
|
5188
|
+
cwd,
|
|
5189
|
+
env,
|
|
5190
|
+
timeoutMs: PREFLIGHT_TIMEOUT_MS
|
|
5191
|
+
});
|
|
5192
|
+
if (gh.code === null || gh.code === 127)
|
|
5193
|
+
return null;
|
|
5194
|
+
if (gh.code !== 0) {
|
|
5195
|
+
return {
|
|
5196
|
+
...provider,
|
|
5197
|
+
body: `The GitHub CLI on runner ${runnerName} is not signed in, so I couldn't open a pull request. Its owner can fix it on that machine \u2014 the link says how. Reply here once it's done and I'll carry on.`
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
5200
|
+
return null;
|
|
5201
|
+
}
|
|
5202
|
+
|
|
5082
5203
|
// src/acp/templates/manager.md
|
|
5083
5204
|
var manager_default = "You are the planning agent for this Linear workspace, acting as the app user `@hypotenuse`, on\n{{issue.identifier}} \u2014 {{issue.title}}. You are an AI agent; say so if anyone asks.\n\n## What you produce\n\n- **A PRD.** When the issue concerns a whole project, write it into the project's content with\n `linear_update_project` (read it first with `linear_get_project`). When a person already owns that content, put\n the PRD in a document attached to the project with `linear_create_document`, and revise it later with\n `linear_update_document`. Sections: Problem, Goals, Non-goals, User stories, Requirements, Open questions,\n Rollout. Read the code in your working directory so the PRD fits the codebase as it is.\n- **A breakdown into issues**, each small enough for one agent session: sub-issues of this issue\n (`linear_create_sub_issue`) or new issues (`linear_create_issue`), edited with `linear_update_issue`. Every issue\n has a title and a description with an **Acceptance criteria** checklist (`- [ ] \u2026`); labels chosen from\n `linear_list_labels`, never invented; an estimate where the team uses them; and `blocks` or `related` relations\n (`linear_create_relation`) where order matters. Look for existing issues first with `linear_search_issues` and\n `linear_get_issue`, so you do not file duplicates.\n\n## How you ask\n\nAsk clarifying questions with `linear_ask_user`, with `options` whenever the answer is a choice. Ask everything you\nneed at once, not one question per turn, then end your turn. Read what people already said with\n`linear_list_comments`.\n\n## What you never do\n\n- Write or change code, or run commands: your working directory is read-only. If answering a question needs\n something run, file a small spike issue and dispatch it.\n- Open or merge pull requests: `linear_report_pr` is for workers, and a worker's pull requests reach you in its\n review.\n- Move an issue to a completed or canceled state with `linear_set_issue_state` unless the tool tells you that is\n permitted. Moving an issue to an \"In Review\"-type started state is fine.\n- Dispatch an issue you did not create, or that a person has not delegated to you or put under this issue.\n- Have more dispatched issues in progress at once than the tool allows; it will tell you when you are at the\n limit.\n\n## How you dispatch\n\nDispatch only issues whose acceptance criteria are written: one `hypotenuse_dispatch` per issue, with\n`instructions` for context the worker needs that does not belong in the issue. A worker session starts on the\nissue on whichever runner can take it. Keep this session's plan current with `linear_set_plan` if your own plan\nupdates do not already appear.\n\n## How you review\n\nWhen a dispatched worker finishes, you get a review turn with its result: its outcome, final response, plan, and\npull requests. Check the result against the issue's acceptance criteria (`linear_get_issue`), then do one of:\n\n- **Accept**: say so in a `linear_comment` on the worker's issue (\"Reviewed against the acceptance criteria: \u2026\"),\n optionally moving it to an \"In Review\"-type state with `linear_set_issue_state`. A person merges the pull\n request.\n- **Request changes**: one `hypotenuse_dispatch` with `mode: \"changes\"` and specific `instructions` \u2014 what is\n missing or wrong, against which criterion. The same worker session continues on the same branch.\n- **File follow-ups**: new issues with `linear_create_issue`, related to the original with\n `linear_create_relation` (`related`). Dispatch them now only if they are ready.\n\nWhen requesting changes is refused because the review round limit is reached, stop sending the work back: end\nwith `linear_ask_user`, addressed to the person who started this session, explaining what is still wrong.\n\nEnd every review turn with a response that says which you chose, and why.\n\n## Writing in Linear\n\nTo mention a person or an issue, use its Linear URL, not an @name. Finish every turn with a short summary of what\nyou decided and what happens next.\n";
|
|
5084
5205
|
|
|
5085
5206
|
// src/acp/templates/worker.md
|
|
5086
|
-
var worker_default =
|
|
5087
|
-
|
|
5088
|
-
- Read the issue and the Linear context below before you change anything, and keep a plan current as you go.
|
|
5089
|
-
\`linear_get_issue\` and \`linear_list_comments\` read the issue and its thread when you need more.
|
|
5090
|
-
- Commit in small steps; reference {{issue.identifier}} in every commit message.
|
|
5091
|
-
- When the work is ready, push the branch and open a pull request that links {{issue.url}}, then call
|
|
5092
|
-
\`linear_report_pr\` with its URL straight away.
|
|
5093
|
-
- If a decision belongs to a person (scope, product behaviour, anything destructive), ask once, clearly, with
|
|
5094
|
-
\`linear_ask_user\` \u2014 with options when there are a few clear choices \u2014 and end your turn there. Their answer
|
|
5095
|
-
arrives as your next prompt.
|
|
5096
|
-
- Your thoughts, tool calls, and final message already appear in Linear. Do not repeat them with \`linear_comment\`;
|
|
5097
|
-
that is for a question someone left elsewhere on the issue, or a blocker for someone not following this session.
|
|
5098
|
-
- To mention a person or an issue in Linear, use its Linear URL, not an @name.
|
|
5099
|
-
- Finish with a short summary: what changed, what you tested, and what a reviewer should look at first. Do not list
|
|
5100
|
-
every file.
|
|
5101
|
-
`;
|
|
5207
|
+
var worker_default = "Implement {{issue.identifier}} \u2014 {{issue.title}} \u2014 in the working directory. You are an AI agent working as the\nLinear app user `@hypotenuse`; say so if anyone asks.\n\n- Read the issue and the Linear context below before you change anything, and keep a plan current as you go.\n `linear_get_issue` and `linear_list_comments` read the issue and its thread when you need more.\n- Commit in small steps; reference {{issue.identifier}} in every commit message.\n- When the work is ready, push the branch and open a pull request that links {{issue.url}}, then call\n `linear_report_pr` with its URL straight away.\n- If a decision belongs to a person (scope, product behaviour, anything destructive), ask once, clearly, with\n `linear_ask_user` \u2014 with options when there are a few clear choices \u2014 and end your turn there. Their answer\n arrives as your next prompt.\n- Your thoughts, tool calls, and final message already appear in Linear. Do not repeat them with `linear_comment`;\n that is for a question someone left elsewhere on the issue, or a blocker for someone not following this session.\n- To mention a person or an issue in Linear, use its Linear URL, not an @name.\n- Finish with a short summary: what changed, what you tested, and what a reviewer should look at first. Do not list\n every file.\n";
|
|
5102
5208
|
|
|
5103
5209
|
// src/acp/prompt.ts
|
|
5104
5210
|
var DEFAULT_TEMPLATES = {
|
|
@@ -5298,6 +5404,21 @@ class TurnFailure extends Error {
|
|
|
5298
5404
|
}
|
|
5299
5405
|
}
|
|
5300
5406
|
|
|
5407
|
+
class AuthRequired extends Error {
|
|
5408
|
+
need;
|
|
5409
|
+
unavailable;
|
|
5410
|
+
constructor(need, unavailable) {
|
|
5411
|
+
super(need.body);
|
|
5412
|
+
this.need = need;
|
|
5413
|
+
this.unavailable = unavailable;
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
var SIGNED_IN_BODY = "Signed in \u2014 continuing.";
|
|
5417
|
+
function resumesAfterAuth(assignment) {
|
|
5418
|
+
const last = (assignment.history ?? []).filter((h) => h.direction === "outbound").at(-1);
|
|
5419
|
+
return last?.type === "elicitation" && last.signal === "auth";
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5301
5422
|
class AcpSessionExecutor {
|
|
5302
5423
|
options;
|
|
5303
5424
|
constructor(options = {}) {
|
|
@@ -5337,6 +5458,7 @@ class Turn {
|
|
|
5337
5458
|
conn = null;
|
|
5338
5459
|
acpSessionId = null;
|
|
5339
5460
|
agentTitle;
|
|
5461
|
+
agentVersion = null;
|
|
5340
5462
|
endReason = null;
|
|
5341
5463
|
ended;
|
|
5342
5464
|
endNow;
|
|
@@ -5446,9 +5568,30 @@ class Turn {
|
|
|
5446
5568
|
});
|
|
5447
5569
|
await this.prepare();
|
|
5448
5570
|
await this.start();
|
|
5571
|
+
if (resumesAfterAuth(ctx.assignment))
|
|
5572
|
+
this.emit({ type: "thought", body: SIGNED_IN_BODY, ephemeral: false });
|
|
5449
5573
|
timer = setInterval(() => this.tick(), this.options.tickMs ?? TICK_MS);
|
|
5450
5574
|
return await this.prompt();
|
|
5451
5575
|
} catch (error) {
|
|
5576
|
+
if (error instanceof AuthRequired && !this.endReason) {
|
|
5577
|
+
if (error.unavailable)
|
|
5578
|
+
ctx.agentAvailability(false, error.unavailable);
|
|
5579
|
+
this.log.note(`needs a login: ${error.need.provider}`);
|
|
5580
|
+
this.emitAll(this.mapper.flushThought(false));
|
|
5581
|
+
this.emit({
|
|
5582
|
+
type: "elicitation",
|
|
5583
|
+
body: error.need.body,
|
|
5584
|
+
signal: "auth",
|
|
5585
|
+
signalMetadata: {
|
|
5586
|
+
provider: error.need.provider,
|
|
5587
|
+
providerName: error.need.providerName.slice(0, 255)
|
|
5588
|
+
}
|
|
5589
|
+
});
|
|
5590
|
+
this.mapper.askedUser = true;
|
|
5591
|
+
return this.result("completed");
|
|
5592
|
+
}
|
|
5593
|
+
if (error instanceof AuthRequired)
|
|
5594
|
+
return await this.stopped();
|
|
5452
5595
|
if (error instanceof TurnFailure) {
|
|
5453
5596
|
if (error.unavailable)
|
|
5454
5597
|
ctx.agentAvailability(false, error.unavailable);
|
|
@@ -5482,6 +5625,14 @@ class Turn {
|
|
|
5482
5625
|
env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
|
|
5483
5626
|
});
|
|
5484
5627
|
} catch (error) {
|
|
5628
|
+
const remote = ctx.assignment.repository?.remote ?? null;
|
|
5629
|
+
if (error instanceof WorkspaceError && remote && isGitAuthFailure(error.body)) {
|
|
5630
|
+
const provider = remoteProvider(remote);
|
|
5631
|
+
throw new AuthRequired({
|
|
5632
|
+
...provider,
|
|
5633
|
+
body: `Runner ${ctx.runnerName} can't sign in to ${provider.providerName} to fetch \`${remote}\`. Its owner can fix it on that machine \u2014 the link says how. Reply here once it's done and I'll carry on.`
|
|
5634
|
+
});
|
|
5635
|
+
}
|
|
5485
5636
|
if (error instanceof WorkspaceError)
|
|
5486
5637
|
throw new TurnFailure(error.body);
|
|
5487
5638
|
throw error;
|
|
@@ -5491,6 +5642,17 @@ class Turn {
|
|
|
5491
5642
|
} else {
|
|
5492
5643
|
mkdirSync6(this.cwd, { recursive: true, mode: 448 });
|
|
5493
5644
|
}
|
|
5645
|
+
if (this.endReason)
|
|
5646
|
+
throw new TurnFailure("stopped");
|
|
5647
|
+
const need = await checkRepositoryAuth({
|
|
5648
|
+
remote: this.workspace?.repository?.remote ?? null,
|
|
5649
|
+
cwd: this.cwd,
|
|
5650
|
+
env: ctx.env,
|
|
5651
|
+
runnerName: ctx.runnerName,
|
|
5652
|
+
pullRequests: (assignment.role ?? "worker") === "worker"
|
|
5653
|
+
}, this.options.run);
|
|
5654
|
+
if (need)
|
|
5655
|
+
throw new AuthRequired(need);
|
|
5494
5656
|
if (this.endReason)
|
|
5495
5657
|
throw new TurnFailure("stopped");
|
|
5496
5658
|
this.mapperOptions.cwd = this.cwd;
|
|
@@ -5566,7 +5728,7 @@ class Turn {
|
|
|
5566
5728
|
throw this.fsError(error);
|
|
5567
5729
|
}
|
|
5568
5730
|
},
|
|
5569
|
-
createElicitation: (params) => this.questions.ask(elicitationQuestion(params, this.agentTitle)),
|
|
5731
|
+
createElicitation: (params) => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId))),
|
|
5570
5732
|
unknown: (method) => this.log.note(`unhandled ${method}`)
|
|
5571
5733
|
}
|
|
5572
5734
|
});
|
|
@@ -5579,6 +5741,7 @@ class Turn {
|
|
|
5579
5741
|
ctx.setState("starting");
|
|
5580
5742
|
const init = await this.guard(conn.initialize(package_default.version), launch.startupTimeoutMs, `${this.agentId} did not answer initialize within ${launch.startupTimeoutMs / 1000}s (\`${launch.command.join(" ")}\`).`, true);
|
|
5581
5743
|
this.agentTitle = init.agentInfo?.title ?? init.agentInfo?.name ?? this.agentId;
|
|
5744
|
+
this.agentVersion = init.agentInfo?.version ?? null;
|
|
5582
5745
|
ctx.log.debug(`${this.agentId} initialized: ${init.agentInfo?.name ?? "?"} ${init.agentInfo?.version ?? ""}`, { session: assignment.agentSessionId });
|
|
5583
5746
|
await this.openSession(init, launch.authMethod);
|
|
5584
5747
|
ctx.agentAvailability(true);
|
|
@@ -5620,21 +5783,30 @@ class Turn {
|
|
|
5620
5783
|
await this.chooseSession(init.agentCapabilities ?? {});
|
|
5621
5784
|
return;
|
|
5622
5785
|
} catch (error) {
|
|
5623
|
-
if (
|
|
5786
|
+
if (isAuthRequired(error) && authenticated) {
|
|
5787
|
+
throw new AuthRequired(this.loginNeed(), "not logged in");
|
|
5788
|
+
}
|
|
5789
|
+
if (!isAuthRequired(error)) {
|
|
5624
5790
|
if (error instanceof TurnFailure)
|
|
5625
5791
|
throw error;
|
|
5626
5792
|
throw new TurnFailure(`${this.agentId} could not open a session: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
|
|
5627
5793
|
}
|
|
5628
5794
|
const method = this.authMethodFor(init.authMethods ?? [], configuredMethod);
|
|
5629
|
-
if (!method)
|
|
5630
|
-
throw new
|
|
5631
|
-
}
|
|
5795
|
+
if (!method)
|
|
5796
|
+
throw new AuthRequired(this.loginNeed(), "not logged in");
|
|
5632
5797
|
this.log.note(`authenticate ${method}`);
|
|
5633
5798
|
await this.guard(this.conn.authenticate(method), 0, "");
|
|
5634
5799
|
authenticated = true;
|
|
5635
5800
|
}
|
|
5636
5801
|
}
|
|
5637
5802
|
}
|
|
5803
|
+
loginNeed() {
|
|
5804
|
+
return {
|
|
5805
|
+
provider: providerSlug(this.agentId),
|
|
5806
|
+
providerName: this.agentTitle,
|
|
5807
|
+
body: `${this.agentTitle} on runner ${this.ctx.runnerName} is not logged in. Its owner can sign in on that machine with \`hypotenuse agents login ${this.agentId}\` \u2014 the link has the steps. Reply here once it's done and I'll carry on.`
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5638
5810
|
authMethodFor(methods, configured) {
|
|
5639
5811
|
const agentRun = methods.filter((m) => m.type !== "terminal");
|
|
5640
5812
|
if (configured)
|
|
@@ -5772,7 +5944,16 @@ class Turn {
|
|
|
5772
5944
|
const stop = answer.stopReason === "cancelled" ? "end_turn" : answer.stopReason;
|
|
5773
5945
|
if (this.mapper.askedUser && this.mapper.hasMessage)
|
|
5774
5946
|
this.log.note(`said after asking (kept out of Linear): ${this.mapper.message().slice(0, 2000)}`);
|
|
5775
|
-
this.emitAll(this.mapper.finish(stop, {
|
|
5947
|
+
this.emitAll(this.mapper.finish(stop, {
|
|
5948
|
+
branch: this.branch,
|
|
5949
|
+
signature: responseSignature({
|
|
5950
|
+
role: this.ctx.assignment.role ?? "worker",
|
|
5951
|
+
agentId: this.agentId,
|
|
5952
|
+
agentTitle: this.agentTitle,
|
|
5953
|
+
agentVersion: this.agentVersion,
|
|
5954
|
+
runnerName: this.ctx.runnerName
|
|
5955
|
+
})
|
|
5956
|
+
}));
|
|
5776
5957
|
return stop === "end_turn" ? this.result("completed") : this.result("failed", `stopReason: ${stop}`);
|
|
5777
5958
|
}
|
|
5778
5959
|
repositoryName() {
|
|
@@ -8094,8 +8275,9 @@ class Supervisor {
|
|
|
8094
8275
|
this.silence(id);
|
|
8095
8276
|
for (const cancel of answer.cancels) {
|
|
8096
8277
|
const slot = this.slots.get(cancel.workItemId);
|
|
8097
|
-
|
|
8098
|
-
|
|
8278
|
+
const note = cancel.by ?? cancel.said;
|
|
8279
|
+
if (slot && !slot.reason && note)
|
|
8280
|
+
slot.killNote = note;
|
|
8099
8281
|
this.stop(cancel.workItemId, cancel.reason);
|
|
8100
8282
|
}
|
|
8101
8283
|
for (const reply of answer.answers ?? [])
|
|
@@ -10248,7 +10430,7 @@ function describeEvent(event, payload) {
|
|
|
10248
10430
|
}
|
|
10249
10431
|
function registerEventCommands(program) {
|
|
10250
10432
|
const event = program.command("event").description("The selected project's event log: Linear's webhooks and how each was routed");
|
|
10251
|
-
withPaging(event.command("list").description("List webhook events, newest first").option("--type <eventType>", "only this event type, e.g. AgentSessionEvent, Issue, PermissionChange").option("--outcome <outcome>", "only this outcome: handled, ignored, duplicate_of_dispatch, failed, pending").option("--hide-ignored", "leave out events Hypotenuse did not act on").option("--session <id>", "only events about this agent session").option("--since <time>", "only events since: ISO-8601, epoch ms, or 7d / 12h / 30m")).action(async function() {
|
|
10433
|
+
withPaging(event.command("list").description("List webhook events, newest first").option("--type <eventType>", "only this event type, e.g. AgentSessionEvent, Issue, PermissionChange").option("--outcome <outcome>", "only this outcome: handled, ignored, duplicate_of_dispatch, noted, mention_without_session, assigned_not_delegated, disengaged, failed, pending").option("--hide-ignored", "leave out events Hypotenuse did not act on").option("--session <id>", "only events about this agent session").option("--since <time>", "only events since: ISO-8601, epoch ms, or 7d / 12h / 30m")).action(async function() {
|
|
10252
10434
|
await runCommand(this, async (ctx) => {
|
|
10253
10435
|
const opts = this.opts();
|
|
10254
10436
|
const projectId = await ctx.projectId();
|