@saadnvd1/agent-os 0.3.659 → 0.3.662
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/app/api/sessions/[id]/summarize/route.ts +3 -2
- package/lib/agents/launch.ts +3 -2
- package/lib/agents/server-env.ts +24 -0
- package/lib/chat/drivers/claude.ts +3 -2
- package/lib/chat/drivers/codex.ts +3 -2
- package/lib/chat/drivers/copilot.ts +2 -1
- package/lib/chat/drivers/opencode.ts +3 -2
- package/lib/chat/drivers/pi.ts +3 -2
- package/lib/chat/worker/client.ts +18 -13
- package/lib/hosts/attach.ts +8 -1
- package/lib/orchestration.ts +3 -1
- package/lib/orchestrator/external-gates.ts +6 -1
- package/lib/orchestrator/review-prompt.ts +1 -1
- package/lib/orchestrator/review.ts +6 -2
- package/lib/pr-generation.ts +2 -1
- package/lib/project-config/agentos.schema.json +25 -0
- package/lib/project-config/brief.ts +14 -0
- package/lib/project-config/index.ts +6 -0
- package/lib/project-config/schema.ts +21 -0
- package/lib/resurrect/index.ts +2 -2
- package/lib/tasks/brief.ts +20 -5
- package/lib/tasks/gh.ts +2 -0
- package/lib/tasks/import.ts +2 -2
- package/lib/tasks/index.ts +2 -2
- package/lib/tasks/move.ts +2 -2
- package/lib/tasks/state.ts +3 -0
- package/lib/tasks/ui-brief.ts +85 -0
- package/lib/tasks/ui-change.ts +57 -0
- package/lib/tmux/exec.ts +3 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { randomUUID } from "crypto";
|
|
|
8
8
|
import { readFile } from "fs/promises";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import { pasteText, tmux } from "@/lib/tmux/exec";
|
|
11
|
+
import { sessionBaseEnv, UNSET_SERVER_ENV } from "@/lib/agents/server-env";
|
|
11
12
|
|
|
12
13
|
// Get Claude session ID from tmux environment
|
|
13
14
|
async function getClaudeSessionId(tmuxSession: string): Promise<string | null> {
|
|
@@ -135,7 +136,7 @@ async function generateSummary(conversation: string): Promise<string> {
|
|
|
135
136
|
const routed = await routedRun("helper");
|
|
136
137
|
return new Promise((resolve, reject) => {
|
|
137
138
|
const claude = spawn("claude", ["-p", prompt, "--model", routed.model], {
|
|
138
|
-
env: { ...
|
|
139
|
+
env: { ...sessionBaseEnv(), ...routed.env },
|
|
139
140
|
stdio: ["pipe", "pipe", "pipe"],
|
|
140
141
|
});
|
|
141
142
|
|
|
@@ -309,7 +310,7 @@ export async function POST(
|
|
|
309
310
|
|
|
310
311
|
// Start new tmux session with Claude directly
|
|
311
312
|
const isRoot = process.getuid?.() === 0;
|
|
312
|
-
const envPrefix = isRoot ? "IS_SANDBOX=1 " : "";
|
|
313
|
+
const envPrefix = UNSET_SERVER_ENV + (isRoot ? "IS_SANDBOX=1 " : "");
|
|
313
314
|
const claudeCmd = effectiveAutoApprove(session.auto_approve)
|
|
314
315
|
? `${envPrefix}claude --dangerously-skip-permissions`
|
|
315
316
|
: "claude";
|
package/lib/agents/launch.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { runInBackground } from "../async-operations";
|
|
|
10
10
|
import { trustPromptKeys } from "../tasks/state";
|
|
11
11
|
import { BUS_BRIEF } from "./brief";
|
|
12
12
|
import { selfUrl } from "./self-env";
|
|
13
|
+
import { sessionBaseEnv, UNSET_SERVER_ENV } from "./server-env";
|
|
13
14
|
import { sessionPath } from "./tool-path";
|
|
14
15
|
import { autoCompactEnv } from "../chat/refresh";
|
|
15
16
|
import { subagentEnv } from "../models/subagents";
|
|
@@ -110,7 +111,7 @@ export async function launchClaude(opts: {
|
|
|
110
111
|
model_provider: opts.provider ?? null,
|
|
111
112
|
});
|
|
112
113
|
const mcpFlags = routedMcpFlags(opts.provider);
|
|
113
|
-
const agent =
|
|
114
|
+
const agent = `${UNSET_SERVER_ENV}export PATH=${shellQuote(AOS_BIN_DIR)}:"$HOME/.local/bin:$PATH"; ${routing.exportKey} ${provider.command} ${flags} ${mcpFlags}${CLAUDE_STATUS_SETTINGS_FLAG} --append-system-prompt-file ${shellQuote(briefFile)} "$(cat ${shellQuote(promptFile)})"; exec "\${SHELL:-/bin/sh}" -l`;
|
|
114
115
|
await execFileAsync(
|
|
115
116
|
"tmux",
|
|
116
117
|
[
|
|
@@ -123,7 +124,7 @@ export async function launchClaude(opts: {
|
|
|
123
124
|
...envArgs({ ...agentEnv(opts.sessionId), ...routing.env }),
|
|
124
125
|
agent,
|
|
125
126
|
],
|
|
126
|
-
{ cwd: opts.cwd }
|
|
127
|
+
{ cwd: opts.cwd, env: sessionBaseEnv() }
|
|
127
128
|
);
|
|
128
129
|
runInBackground(
|
|
129
130
|
() => acceptTrustPrompt(opts.tmuxName),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Variables the server sets only for itself (`npm start`, `npm run dev`,
|
|
2
|
+
// the dev-instance spawner). A session that inherits them breaks the
|
|
3
|
+
// project it works in: NODE_ENV=production makes Vitest load React's
|
|
4
|
+
// production build, PORT makes a dev server bind the live one's port.
|
|
5
|
+
// AGENTOS_URL, AGENTOS_PORT and the session ids are not here: sessions need them.
|
|
6
|
+
export const SERVER_ONLY_ENV = [
|
|
7
|
+
"NODE_ENV",
|
|
8
|
+
"PORT",
|
|
9
|
+
"AGENTOS_SERVER",
|
|
10
|
+
"AGENTOS_DEV",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
// Shell text that clears them inside a pane. `tmux new-session -e` can only
|
|
14
|
+
// add variables, and an already-running tmux server may hold them globally.
|
|
15
|
+
export const UNSET_SERVER_ENV = `unset ${SERVER_ONLY_ENV.join(" ")}; `;
|
|
16
|
+
|
|
17
|
+
// The server's environment as an agent or its tools should see it.
|
|
18
|
+
export function sessionBaseEnv(
|
|
19
|
+
env: NodeJS.ProcessEnv = process.env
|
|
20
|
+
): NodeJS.ProcessEnv {
|
|
21
|
+
const out = { ...env };
|
|
22
|
+
for (const k of SERVER_ONLY_ENV) delete out[k];
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import { randomUUID } from "crypto";
|
|
2
3
|
import {
|
|
3
4
|
getSessionMessages,
|
|
@@ -145,7 +146,7 @@ export const claudeDriver: ChatDriver = {
|
|
|
145
146
|
cwd,
|
|
146
147
|
permissionMode: "bypassPermissions",
|
|
147
148
|
allowDangerouslySkipPermissions: true,
|
|
148
|
-
env: { ...
|
|
149
|
+
env: { ...sessionBaseEnv(), ...env },
|
|
149
150
|
},
|
|
150
151
|
});
|
|
151
152
|
try {
|
|
@@ -235,7 +236,7 @@ export const claudeDriver: ChatDriver = {
|
|
|
235
236
|
append: options.systemAppend,
|
|
236
237
|
},
|
|
237
238
|
env: {
|
|
238
|
-
...
|
|
239
|
+
...sessionBaseEnv(),
|
|
239
240
|
...options.env,
|
|
240
241
|
// When the agent is done with every turn it had queued: the end
|
|
241
242
|
// of a turn that never sent its result.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -16,7 +17,7 @@ export const codexDriver: ChatDriver = {
|
|
|
16
17
|
|
|
17
18
|
async discover({ cwd, env }) {
|
|
18
19
|
const rpc = new CodexRpc(
|
|
19
|
-
{ cwd, env: { ...
|
|
20
|
+
{ cwd, env: { ...sessionBaseEnv(), ...env } },
|
|
20
21
|
{ notify: () => {}, request: () => {}, exit: () => {} }
|
|
21
22
|
);
|
|
22
23
|
try {
|
|
@@ -51,7 +52,7 @@ export const codexDriver: ChatDriver = {
|
|
|
51
52
|
let starting = false;
|
|
52
53
|
let stopOnStart = false;
|
|
53
54
|
const rpc: CodexRpc = new CodexRpc(
|
|
54
|
-
{ cwd: options.cwd, env: { ...
|
|
55
|
+
{ cwd: options.cwd, env: { ...sessionBaseEnv(), ...options.env } },
|
|
55
56
|
{
|
|
56
57
|
notify(method, params) {
|
|
57
58
|
if (method === "item/started") approvals.sawItem(params.item as P);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver, ChatStartOptions } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -52,7 +53,7 @@ export const copilotDriver: ChatDriver = {
|
|
|
52
53
|
const held: { text: string; images?: ChatImage[] }[] = [];
|
|
53
54
|
const rpc: CopilotRpc = new CopilotRpc(
|
|
54
55
|
copilotArgs(options),
|
|
55
|
-
{ cwd: options.cwd, env: { ...
|
|
56
|
+
{ cwd: options.cwd, env: { ...sessionBaseEnv(), ...options.env } },
|
|
56
57
|
{
|
|
57
58
|
notify(method, params) {
|
|
58
59
|
if (method === "session/update")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -46,7 +47,7 @@ export const openCodeDriver: ChatDriver = {
|
|
|
46
47
|
atRest: false,
|
|
47
48
|
|
|
48
49
|
async discover({ cwd, env }) {
|
|
49
|
-
const server = new OpenCodeServer(cwd, { ...
|
|
50
|
+
const server = new OpenCodeServer(cwd, { ...sessionBaseEnv(), ...env });
|
|
50
51
|
try {
|
|
51
52
|
const [providers, commands] = await Promise.all([
|
|
52
53
|
server.call<{ all: P[]; connected: string[] }>("GET", "/provider"),
|
|
@@ -79,7 +80,7 @@ export const openCodeDriver: ChatDriver = {
|
|
|
79
80
|
const out = new InputQueue<DriverEvent>();
|
|
80
81
|
const emit = (e: DriverEvent) => out.push(e);
|
|
81
82
|
const server = new OpenCodeServer(options.cwd, {
|
|
82
|
-
...
|
|
83
|
+
...sessionBaseEnv(),
|
|
83
84
|
...options.env,
|
|
84
85
|
});
|
|
85
86
|
let sessionId: string | null = null;
|
package/lib/chat/drivers/pi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver, ChatStartOptions } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -45,7 +46,7 @@ export const piDriver: ChatDriver = {
|
|
|
45
46
|
async discover({ cwd, env }) {
|
|
46
47
|
const rpc = new PiRpc(
|
|
47
48
|
["--no-session", "--offline"],
|
|
48
|
-
{ cwd, env: { ...
|
|
49
|
+
{ cwd, env: { ...sessionBaseEnv(), ...env } },
|
|
49
50
|
{ event: () => {}, ui: () => {}, exit: () => {} }
|
|
50
51
|
);
|
|
51
52
|
try {
|
|
@@ -82,7 +83,7 @@ export const piDriver: ChatDriver = {
|
|
|
82
83
|
{
|
|
83
84
|
cwd: options.cwd,
|
|
84
85
|
env: {
|
|
85
|
-
...
|
|
86
|
+
...sessionBaseEnv(),
|
|
86
87
|
...options.env,
|
|
87
88
|
AGENTOS_PI_ACCESS_FILE: access.file,
|
|
88
89
|
PI_OFFLINE: "1",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UNSET_SERVER_ENV, sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
/**
|
|
2
3
|
* The server's side of a chat worker: starts one in tmux when none is
|
|
3
4
|
* running, connects to its socket, and passes commands and events along.
|
|
@@ -224,7 +225,7 @@ async function spawnWorker(sessionId: string): Promise<void> {
|
|
|
224
225
|
if (await tmuxHas(name)) return;
|
|
225
226
|
fs.mkdirSync(workerDir(), { recursive: true, mode: 0o700 });
|
|
226
227
|
const env: Record<string, string | undefined> = {
|
|
227
|
-
...
|
|
228
|
+
...sessionBaseEnv(),
|
|
228
229
|
DB_PATH: path.resolve(
|
|
229
230
|
process.env.DB_PATH || path.join(process.cwd(), "agent-os.db")
|
|
230
231
|
),
|
|
@@ -233,18 +234,22 @@ async function spawnWorker(sessionId: string): Promise<void> {
|
|
|
233
234
|
delete env.TMUX_PANE;
|
|
234
235
|
fs.writeFileSync(envPath(sessionId), JSON.stringify(env), { mode: 0o600 });
|
|
235
236
|
const entry = path.join(process.cwd(), "lib/chat/worker/main.ts");
|
|
236
|
-
await execFileAsync(
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
237
|
+
await execFileAsync(
|
|
238
|
+
"tmux",
|
|
239
|
+
[
|
|
240
|
+
"new-session",
|
|
241
|
+
"-d",
|
|
242
|
+
"-s",
|
|
243
|
+
name,
|
|
244
|
+
"-c",
|
|
245
|
+
process.cwd(),
|
|
246
|
+
// The worker finds its env file by DB_PATH, so it can't come from there.
|
|
247
|
+
"-e",
|
|
248
|
+
`DB_PATH=${env.DB_PATH}`,
|
|
249
|
+
`${UNSET_SERVER_ENV}exec ${shellQuote(process.execPath)} --import tsx ${shellQuote(entry)} ${shellQuote(sessionId)} > ${shellQuote(path.join(workerDir(), `${sessionId}.log`))} 2>&1`,
|
|
250
|
+
],
|
|
251
|
+
{ env: sessionBaseEnv() }
|
|
252
|
+
);
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
package/lib/hosts/attach.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SSH_OPTIONS, loginShellCommand, shellQuote } from "./ssh";
|
|
2
2
|
import { isValidTmuxName } from "./tmux-name";
|
|
3
|
+
import { SERVER_ONLY_ENV } from "../agents/server-env";
|
|
4
|
+
|
|
5
|
+
// For a shared tmux server that already holds them: attach is the one path
|
|
6
|
+
// that cannot prefix the pane command.
|
|
7
|
+
const UNSET_GLOBAL_ENV = SERVER_ONLY_ENV.map(
|
|
8
|
+
(k) => `set-environment -gu ${k} \\;`
|
|
9
|
+
).join(" ");
|
|
3
10
|
|
|
4
11
|
export interface AttachSpec {
|
|
5
12
|
sessionName: string;
|
|
@@ -28,7 +35,7 @@ export function buildTmuxAttachCommand(spec: AttachSpec): string {
|
|
|
28
35
|
}
|
|
29
36
|
// Commands chained after an attaching command only run once the client
|
|
30
37
|
// detaches, so options go first; start-server keeps a fresh server alive.
|
|
31
|
-
const prefix =
|
|
38
|
+
const prefix = `tmux start-server \\; set -g mouse on \\; ${UNSET_GLOBAL_ENV}`;
|
|
32
39
|
if (spec.attachOnly) {
|
|
33
40
|
// Quoted: zsh expands a bare "=word" to a command path.
|
|
34
41
|
return `${prefix} attach-session -t ${shellQuote(`=${spec.sessionName}`)}`;
|
package/lib/orchestration.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { resolveModelForAgent } from "./model-catalog";
|
|
|
15
15
|
import { type AgentType, getProvider } from "./providers";
|
|
16
16
|
import { statusDetector } from "./status-detector";
|
|
17
17
|
import { wrapWithBanner } from "./banner";
|
|
18
|
+
import { UNSET_SERVER_ENV } from "./agents/server-env";
|
|
18
19
|
import { runInBackground } from "./async-operations";
|
|
19
20
|
import { tmux } from "./tmux/exec";
|
|
20
21
|
import { endTmuxSession } from "./tmux/end";
|
|
@@ -168,7 +169,8 @@ export async function spawnWorker(
|
|
|
168
169
|
|
|
169
170
|
// Create tmux session with the agent and banner
|
|
170
171
|
const agentCmd = `${provider.command} ${flagsStr}`;
|
|
171
|
-
const newSessionCmd =
|
|
172
|
+
const newSessionCmd =
|
|
173
|
+
UNSET_SERVER_ENV + wrapWithBanner(agentCmd, provider.preamble);
|
|
172
174
|
try {
|
|
173
175
|
await tmux(["set", "-g", "mouse", "on"]).catch(() => {});
|
|
174
176
|
// The last argument is the agent's command line, which tmux runs.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* does carry is held to the same rules as a task's.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { hasUiEvidence } from "../tasks/ui-change";
|
|
13
14
|
import { MERGED_AS, mergePolicy } from "../tasks/merge-policy";
|
|
14
15
|
import { mergePR } from "../tasks/merge-pr";
|
|
15
16
|
import { attributionIn, codeReviewGate } from "../tasks/code-review";
|
|
@@ -171,7 +172,11 @@ export async function reviewExternal(
|
|
|
171
172
|
goal: `${ext.title}\n\n${ext.body}`.slice(0, GOAL_CAP),
|
|
172
173
|
repo: ext.repo,
|
|
173
174
|
refs: refsOf(ext),
|
|
174
|
-
pr: {
|
|
175
|
+
pr: {
|
|
176
|
+
number: ext.number,
|
|
177
|
+
url: ext.url,
|
|
178
|
+
uiEvidence: hasUiEvidence(ext.body),
|
|
179
|
+
},
|
|
175
180
|
},
|
|
176
181
|
ext.pr.head,
|
|
177
182
|
opts
|
|
@@ -7,7 +7,7 @@ import type { ChangedFile } from "./diff";
|
|
|
7
7
|
|
|
8
8
|
export const REVIEW_SYSTEM = `You are an independent code reviewer. You did not write this change and you owe its author nothing. You review one pull request at one exact commit, checked out read-only in your working directory. You can read the checkout's files; you cannot edit anything or run commands.
|
|
9
9
|
|
|
10
|
-
Look for what would be wrong if this merged: bugs, broken behaviour, missing error handling on real paths, security problems (secrets, injection, unsafe shell or SQL), data loss, and changes that don't do what the task asked. Style and taste are never blocking.
|
|
10
|
+
Look for what would be wrong if this merged: bugs, broken behaviour, missing error handling on real paths, security problems (secrets, injection, unsafe shell or SQL), data loss, and changes that don't do what the task asked. Style and taste are never blocking, and neither is a missing screenshot or browser or simulator check: AgentOS notes that itself.
|
|
11
11
|
|
|
12
12
|
A change that games a gate instead of passing it is always blocking: tests deleted, skipped or weakened, assertions loosened or changed to match wrong output, checks, lint rules or CI steps disabled or bypassed, or anything else that makes a failure stop showing rather than stop happening. Name what was weakened, at its file and line.
|
|
13
13
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* the orchestrator hears the verdict as an event.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { uiEvidenceNote, withUiEvidenceNote } from "../tasks/ui-change";
|
|
14
15
|
import { reviewGoal } from "../plans/features";
|
|
15
16
|
import { pushReviewBlocked, pushReviewReady } from "../notify/push/events";
|
|
16
17
|
import { db, type Session } from "../db";
|
|
@@ -89,7 +90,7 @@ export interface ReviewTarget {
|
|
|
89
90
|
goal: string;
|
|
90
91
|
repo: string;
|
|
91
92
|
refs: Pick<Session, "base_branch" | "branch_name">;
|
|
92
|
-
pr: Pick<TaskPR, "number" | "url" | "scopeChange">;
|
|
93
|
+
pr: Pick<TaskPR, "number" | "url" | "scopeChange" | "uiEvidence">;
|
|
93
94
|
task?: Session;
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -192,7 +193,10 @@ async function reviewJob(
|
|
|
192
193
|
const done = putCheck({
|
|
193
194
|
...row,
|
|
194
195
|
kind: "review",
|
|
195
|
-
...
|
|
196
|
+
...withUiEvidenceNote(
|
|
197
|
+
combineVerdicts(verdicts),
|
|
198
|
+
uiEvidenceNote(files, pr.uiEvidence)
|
|
199
|
+
),
|
|
196
200
|
});
|
|
197
201
|
if (task?.lh_card_id)
|
|
198
202
|
await checkScope({
|
package/lib/pr-generation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "./agents/server-env";
|
|
1
2
|
import { routedRun } from "./models/launch";
|
|
2
3
|
import { execFile } from "child_process";
|
|
3
4
|
import { git as runGit } from "./git-exec";
|
|
@@ -163,7 +164,7 @@ async function claudePrint(cwd: string, prompt: string): Promise<string> {
|
|
|
163
164
|
["--print", "--model", routed.model],
|
|
164
165
|
{
|
|
165
166
|
cwd,
|
|
166
|
-
env: { ...
|
|
167
|
+
env: { ...sessionBaseEnv(), ...routed.env },
|
|
167
168
|
encoding: "utf-8",
|
|
168
169
|
timeout: 30_000,
|
|
169
170
|
killSignal: "SIGKILL",
|
|
@@ -142,6 +142,31 @@
|
|
|
142
142
|
"port": {
|
|
143
143
|
"type": "string",
|
|
144
144
|
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
145
|
+
},
|
|
146
|
+
"viewports": {
|
|
147
|
+
"minItems": 1,
|
|
148
|
+
"maxItems": 4,
|
|
149
|
+
"type": "array",
|
|
150
|
+
"items": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"pattern": "^\\d{3,4}x\\d{3,4}$"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"nav": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"propertyNames": {
|
|
158
|
+
"type": "string",
|
|
159
|
+
"minLength": 1,
|
|
160
|
+
"maxLength": 40
|
|
161
|
+
},
|
|
162
|
+
"additionalProperties": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"minLength": 1,
|
|
165
|
+
"maxLength": 200
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"verify": {
|
|
169
|
+
"type": "boolean"
|
|
145
170
|
}
|
|
146
171
|
},
|
|
147
172
|
"additionalProperties": false
|
|
@@ -50,6 +50,20 @@ export function runningBrief(
|
|
|
50
50
|
lines.push(
|
|
51
51
|
`- \`${browse.map}\` maps every page to its URL: navigate by URL, don't explore.`
|
|
52
52
|
);
|
|
53
|
+
const pages = Object.entries(browse?.nav ?? {});
|
|
54
|
+
if (pages.length)
|
|
55
|
+
lines.push(
|
|
56
|
+
`- Pages: ${pages
|
|
57
|
+
.map(([name, to]) => {
|
|
58
|
+
const url = /^https?:\/\//.test(to)
|
|
59
|
+
? to
|
|
60
|
+
: port
|
|
61
|
+
? `http://localhost:${port}${to.startsWith("/") ? "" : "/"}${to}`
|
|
62
|
+
: to;
|
|
63
|
+
return `${name} \`${withPorts(url, ports)}\``;
|
|
64
|
+
})
|
|
65
|
+
.join(", ")}.`
|
|
66
|
+
);
|
|
53
67
|
if (browse)
|
|
54
68
|
lines.push(
|
|
55
69
|
"- Check state with the browser's text snapshot, not screenshots. Screenshot only the final changed screen, and just the changed element (its uid) rather than the full page where the tool allows; every image stays in your context."
|
|
@@ -200,3 +200,9 @@ export function projectEnv(
|
|
|
200
200
|
env[name] = String(port);
|
|
201
201
|
return env;
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
export const DEFAULT_VIEWPORTS = ["1440x900", "390x844"];
|
|
205
|
+
|
|
206
|
+
// The sizes a UI change is checked at: the project's, else desktop and phone.
|
|
207
|
+
export const viewportsOf = (browse: ProjectConfig["browse"]): string[] =>
|
|
208
|
+
browse?.viewports?.length ? browse.viewports : DEFAULT_VIEWPORTS;
|
|
@@ -67,6 +67,27 @@ const browse = z.strictObject({
|
|
|
67
67
|
login: z.string().min(1).optional(),
|
|
68
68
|
map: projectPath.optional(),
|
|
69
69
|
port: envName.optional(),
|
|
70
|
+
// Screen sizes a UI change is checked at, "WIDTHxHEIGHT".
|
|
71
|
+
viewports: z
|
|
72
|
+
.array(
|
|
73
|
+
z
|
|
74
|
+
.string()
|
|
75
|
+
.regex(/^\d{3,4}x\d{3,4}$/, "must be WIDTHxHEIGHT, like 390x844")
|
|
76
|
+
)
|
|
77
|
+
.min(1)
|
|
78
|
+
.max(4)
|
|
79
|
+
.optional(),
|
|
80
|
+
// A short page name to path map, for the pages worth knowing by heart.
|
|
81
|
+
nav: z
|
|
82
|
+
.record(z.string().min(1).max(40), z.string().min(1).max(200))
|
|
83
|
+
.refine(
|
|
84
|
+
(n) => Object.keys(n).length <= 15,
|
|
85
|
+
"at most 15 pages; use map for more"
|
|
86
|
+
)
|
|
87
|
+
.optional(),
|
|
88
|
+
// Force the browser-check section of a task's brief on or off; otherwise
|
|
89
|
+
// the project's files decide (lib/tasks/ui-brief.ts).
|
|
90
|
+
verify: z.boolean().optional(),
|
|
70
91
|
});
|
|
71
92
|
|
|
72
93
|
// An iOS simulator tasks may drive on a Mac (lib/simulator). `start` runs in
|
package/lib/resurrect/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
transcriptPath,
|
|
34
34
|
writeTranscript,
|
|
35
35
|
} from "../tasks/transcript";
|
|
36
|
-
import { buildTaskBrief,
|
|
36
|
+
import { buildTaskBrief, briefFacts } from "../tasks/brief";
|
|
37
37
|
import { syncTaskCardInBackground } from "../lumifyhub/task-cards";
|
|
38
38
|
import { allocatePorts } from "../ports";
|
|
39
39
|
import { loadProjectConfig, portBases } from "../project-config";
|
|
@@ -440,7 +440,7 @@ async function relaunch(s: Session, cwd: string): Promise<void> {
|
|
|
440
440
|
? buildTaskBrief({
|
|
441
441
|
branch: s.branch_name,
|
|
442
442
|
baseBranch: s.base_branch ?? "main",
|
|
443
|
-
|
|
443
|
+
...briefFacts(cwd),
|
|
444
444
|
})
|
|
445
445
|
: undefined,
|
|
446
446
|
});
|
package/lib/tasks/brief.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { loadProjectConfig } from "../project-config";
|
|
1
|
+
import { loadProjectConfig, type ProjectConfig } from "../project-config";
|
|
2
|
+
import { uiKindOf, uiVerifyBrief, type UiKind } from "./ui-brief";
|
|
2
3
|
|
|
3
4
|
// A task stacked on another task's unmerged branch.
|
|
4
5
|
export interface StackedOn {
|
|
@@ -21,9 +22,18 @@ function stackedLines(baseBranch: string, stack: StackedOn): string {
|
|
|
21
22
|
- Never merge or rebase onto the default branch yourself. When the parent merges, AgentOS rebases your branch and retargets your PR, then tells you; re-run your checks when it does. AgentOS reviews your PR itself; a review of your head still counts for the head its restack left, and a push after that gets a new one.${also}`;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
export function
|
|
26
|
-
|
|
25
|
+
// What a brief needs from the checkout's config, from one read.
|
|
26
|
+
export function briefFacts(cwd: string): {
|
|
27
|
+
test?: string;
|
|
28
|
+
ui: UiKind | null;
|
|
29
|
+
browse: ProjectConfig["browse"];
|
|
30
|
+
} {
|
|
31
|
+
const { config } = loadProjectConfig(cwd);
|
|
32
|
+
return {
|
|
33
|
+
test: config.test || undefined,
|
|
34
|
+
ui: uiKindOf(cwd, config),
|
|
35
|
+
browse: config.browse,
|
|
36
|
+
};
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
function checksLine(test?: string): string {
|
|
@@ -40,8 +50,11 @@ export function buildTaskBrief(opts: {
|
|
|
40
50
|
stack?: StackedOn;
|
|
41
51
|
// The project's `test` from agentos.json, when it declares one.
|
|
42
52
|
test?: string;
|
|
53
|
+
// Whether the project has a UI to check, and how it is browsed.
|
|
54
|
+
ui?: UiKind | null;
|
|
55
|
+
browse?: ProjectConfig["browse"];
|
|
43
56
|
}): string {
|
|
44
|
-
|
|
57
|
+
const base = `You are running as an AgentOS background task, alone, in an isolated git worktree.
|
|
45
58
|
|
|
46
59
|
- Branch: ${opts.branch} (based on ${opts.baseBranch}). Stay in this directory and on this branch.
|
|
47
60
|
- Do the task fully. ${checksLine(opts.test)}
|
|
@@ -51,4 +64,6 @@ export function buildTaskBrief(opts: {
|
|
|
51
64
|
- Write scratch files (PR bodies, review notes, logs, test output) outside the worktree, in your scratchpad or \$TMPDIR. Anything uncommitted left in the worktree keeps it from being removed when the task is done.
|
|
52
65
|
- Do not add any AI or assistant attribution to commits or the pull request.
|
|
53
66
|
- If you are blocked or need a decision only a human can make, print one line starting with "BLOCKED:" that says what you need, then stop and wait.`;
|
|
67
|
+
const ui = uiVerifyBrief(opts.ui ?? null, opts.browse);
|
|
68
|
+
return ui ? `${base}\n\n${ui}` : base;
|
|
54
69
|
}
|
package/lib/tasks/gh.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
|
+
import { hasUiEvidence } from "./ui-change";
|
|
2
3
|
import { promisify } from "util";
|
|
3
4
|
import { git } from "../git-exec";
|
|
4
5
|
import { attributionIn } from "./code-review";
|
|
@@ -118,6 +119,7 @@ export async function toTaskPR(repoDir: string, pr: ListedPR): Promise<TaskPR> {
|
|
|
118
119
|
attribution:
|
|
119
120
|
pr.body === undefined ? undefined : (attributionIn(pr.body) ?? null),
|
|
120
121
|
scopeChange: parseScopeChange(pr.body),
|
|
122
|
+
...(pr.body !== undefined && { uiEvidence: hasUiEvidence(pr.body) }),
|
|
121
123
|
...(pr.mergeable && { mergeable: pr.mergeable }),
|
|
122
124
|
...(pr.baseRefName && { base: pr.baseRefName }),
|
|
123
125
|
};
|
package/lib/tasks/import.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { resolveModelForAgent } from "../model-catalog";
|
|
|
23
23
|
import { providerFor } from "../models/resolve";
|
|
24
24
|
import { preparePlanDir, PLAN_FILE } from "../models/plan-step";
|
|
25
25
|
import { carryTaskState } from "./move-carry";
|
|
26
|
-
import { buildTaskBrief,
|
|
26
|
+
import { buildTaskBrief, briefFacts } from "./brief";
|
|
27
27
|
import { expandHome } from "./session";
|
|
28
28
|
import { ensureProject } from "./project-ref";
|
|
29
29
|
import { checkoutBranchWorktree } from "./branch-worktree";
|
|
@@ -141,7 +141,7 @@ async function arrive(
|
|
|
141
141
|
const brief = buildTaskBrief({
|
|
142
142
|
branch: bundle.branch,
|
|
143
143
|
baseBranch,
|
|
144
|
-
|
|
144
|
+
...briefFacts(worktreePath),
|
|
145
145
|
});
|
|
146
146
|
// Checked and written with no await between, so two can't both pass.
|
|
147
147
|
db.transaction(() => {
|
package/lib/tasks/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { agentLabel } from "../agent-options";
|
|
|
33
33
|
import { supportsChat } from "../chat/capabilities";
|
|
34
34
|
import { AGENT_DEFAULT_MODEL } from "../providers/registry";
|
|
35
35
|
import { statusDetector } from "../status-detector";
|
|
36
|
-
import { buildTaskBrief,
|
|
36
|
+
import { buildTaskBrief, briefFacts, type StackedOn } from "./brief";
|
|
37
37
|
import {
|
|
38
38
|
attachTaskCard,
|
|
39
39
|
inBackground,
|
|
@@ -382,7 +382,7 @@ async function startTask(
|
|
|
382
382
|
branch: wt.branchName,
|
|
383
383
|
baseBranch,
|
|
384
384
|
stack: opts.base?.stack,
|
|
385
|
-
|
|
385
|
+
...briefFacts(wt.worktreePath),
|
|
386
386
|
}),
|
|
387
387
|
task_status: "running",
|
|
388
388
|
model_overrides: routing.override
|
package/lib/tasks/move.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { db, type Session } from "../db";
|
|
|
13
13
|
import { getProject } from "../projects";
|
|
14
14
|
import { endTmuxSession } from "../tmux/end";
|
|
15
15
|
import { launchClaude } from "../agents/launch";
|
|
16
|
-
import { buildTaskBrief,
|
|
16
|
+
import { buildTaskBrief, briefFacts } from "./brief";
|
|
17
17
|
import { git as runGit } from "../git-exec";
|
|
18
18
|
import { getTaskSession, isoUTC } from "./session";
|
|
19
19
|
import { projectRef } from "./project-ref";
|
|
@@ -309,7 +309,7 @@ async function relaunch(session: Session, note?: string): Promise<void> {
|
|
|
309
309
|
? buildTaskBrief({
|
|
310
310
|
branch: session.branch_name,
|
|
311
311
|
baseBranch: session.base_branch ?? "main",
|
|
312
|
-
|
|
312
|
+
...briefFacts(session.working_directory),
|
|
313
313
|
})
|
|
314
314
|
: undefined,
|
|
315
315
|
});
|
package/lib/tasks/state.ts
CHANGED
|
@@ -27,6 +27,9 @@ export interface TaskPR {
|
|
|
27
27
|
attribution?: AttributionKind | null;
|
|
28
28
|
// The body's "Scope change" note (tasks/scope-change.ts), when it has one.
|
|
29
29
|
scopeChange?: string | null;
|
|
30
|
+
// Whether the body shows a browser or simulator check (tasks/ui-change.ts);
|
|
31
|
+
// undefined when the body wasn't read.
|
|
32
|
+
uiEvidence?: boolean;
|
|
30
33
|
// GitHub's word on merging it into its base: CONFLICTING once the base
|
|
31
34
|
// moved under it (UNKNOWN while GitHub works it out).
|
|
32
35
|
mergeable?: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The browser-check section of a task's brief, ported from dispatch's
|
|
2
|
+
// browser_brief. A project's files decide whether a task gets it, not the
|
|
3
|
+
// prompt: the PRs that skipped a check were ones whose prompt never said UI.
|
|
4
|
+
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { viewportsOf } from "../project-config";
|
|
8
|
+
import type { ProjectConfig } from "../project-config";
|
|
9
|
+
|
|
10
|
+
export type UiKind = "web" | "simulator";
|
|
11
|
+
|
|
12
|
+
const WEB_DEPS = [
|
|
13
|
+
"react",
|
|
14
|
+
"next",
|
|
15
|
+
"vue",
|
|
16
|
+
"nuxt",
|
|
17
|
+
"svelte",
|
|
18
|
+
"@sveltejs/kit",
|
|
19
|
+
"astro",
|
|
20
|
+
"solid-js",
|
|
21
|
+
"preact",
|
|
22
|
+
"@angular/core",
|
|
23
|
+
];
|
|
24
|
+
const NATIVE_DEPS = ["react-native", "expo"];
|
|
25
|
+
|
|
26
|
+
function deps(cwd: string): Set<string> {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(
|
|
29
|
+
fs.readFileSync(path.join(cwd, "package.json"), "utf8")
|
|
30
|
+
);
|
|
31
|
+
return new Set([
|
|
32
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
33
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
34
|
+
]);
|
|
35
|
+
} catch {
|
|
36
|
+
return new Set();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRailsApp(cwd: string): boolean {
|
|
41
|
+
try {
|
|
42
|
+
return (
|
|
43
|
+
/^\s*gem\s+["']rails["']/m.test(
|
|
44
|
+
fs.readFileSync(path.join(cwd, "Gemfile"), "utf8")
|
|
45
|
+
) && fs.statSync(path.join(cwd, "app", "views")).isDirectory()
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Deterministic and cheap: the config, then two small file reads.
|
|
53
|
+
export function uiKindOf(cwd: string, config: ProjectConfig): UiKind | null {
|
|
54
|
+
if (config.browse?.verify === false) return null;
|
|
55
|
+
if (config.simulator) return "simulator";
|
|
56
|
+
if (config.browse) return "web";
|
|
57
|
+
const have = deps(cwd);
|
|
58
|
+
if (NATIVE_DEPS.some((d) => have.has(d))) return null;
|
|
59
|
+
if (WEB_DEPS.some((d) => have.has(d)) || isRailsApp(cwd)) return "web";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const SCREENSHOT_DIR = ".agentos/screenshots";
|
|
64
|
+
|
|
65
|
+
export function uiVerifyBrief(
|
|
66
|
+
kind: UiKind | null,
|
|
67
|
+
browse?: ProjectConfig["browse"]
|
|
68
|
+
): string {
|
|
69
|
+
if (!kind) return "";
|
|
70
|
+
if (kind === "simulator")
|
|
71
|
+
return `## Checking UI changes
|
|
72
|
+
|
|
73
|
+
If your diff changes what a user sees, check the changed screens in the iOS simulator, as "Running this project" describes, before calling it done. Fix anything visibly off. Add a **Simulator check** section to the PR body: the screens you checked, what you looked for, and the screenshot names. If you didn't check, say why in one line.`;
|
|
74
|
+
const sizes = viewportsOf(browse).join(" and ");
|
|
75
|
+
return `## Checking UI changes
|
|
76
|
+
|
|
77
|
+
If your diff changes what a user sees, run the app and look at every changed screen in a real browser before calling it done, at ${sizes} (\`resize_page\` between them). Do it cheaply:
|
|
78
|
+
|
|
79
|
+
- The browser is the **\`chrome-devtools\`** MCP server. Load \`mcp__chrome-devtools__new_page\`, \`navigate_page\`, \`resize_page\`, \`take_snapshot\`, \`take_screenshot\` and \`evaluate_script\` with ToolSearch. Don't load or call \`mcp__claude-in-chrome__*\`: it isn't configured here and its first call hangs for 30 seconds. If no \`chrome-devtools\` server is available, say so in the PR and describe what you checked instead.
|
|
80
|
+
- Log in and go to pages by URL, from the login, pages and map under "Running this project" when they are there. Exploring the UI to find a page is the most expensive thing a session does.
|
|
81
|
+
- One snapshot to find a uid, then act. Verify with a screenshot of the one element or a small \`evaluate_script\` returning the one value you need, not another full snapshot.
|
|
82
|
+
- Verify the thing that would be wrong, not the thing that is easy to see: a page that rendered proves it rendered. Fix anything visibly off (gaps, clipping, overlap, horizontal scroll, stray bands, misalignment; measure rects with \`evaluate_script\` and report numbers) and read the console for errors.
|
|
83
|
+
- Save screenshots of the final changed screens with \`filePath\` under \`${SCREENSHOT_DIR}/\`, after \`mkdir -p ${SCREENSHOT_DIR} && printf '*\\n' > .agentos/.gitignore\`. That folder is inside the browser's allowed root and git ignores it; a path in /tmp is refused.
|
|
84
|
+
- Add a **Browser check** section to the PR body: the pages and widths you checked, what you fixed, and the screenshot file names. If you didn't check (no visible change, or no browser), say why in one line.`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Whether a change touches what a user sees, and whether its PR body shows
|
|
2
|
+
// anyone looked. The note it makes is for the reviewer's detail only: UI
|
|
3
|
+
// screenshots are not a merge gate, so it can never change a verdict.
|
|
4
|
+
|
|
5
|
+
import type { ChangedFile } from "../orchestrator/diff";
|
|
6
|
+
import type { CheckStatus } from "../orchestrator/checks";
|
|
7
|
+
import { renderedLines } from "./code-review";
|
|
8
|
+
|
|
9
|
+
const UI_EXT =
|
|
10
|
+
/\.(tsx|jsx|vue|svelte|astro|css|scss|sass|less|html|erb|haml|slim)$/i;
|
|
11
|
+
const UI_DIR = /(^|\/)(app\/views|app\/components)\//;
|
|
12
|
+
const SWIFT_VIEW = /(^|\/)(Views?\/.*|\w*Views?)\.swift$/;
|
|
13
|
+
const NOT_UI = /(\.(test|spec|stories)\.|(^|\/)(__tests__|spec|tests?|e2e)\/)/;
|
|
14
|
+
|
|
15
|
+
export function isUiPath(file: string): boolean {
|
|
16
|
+
if (NOT_UI.test(file)) return false;
|
|
17
|
+
return UI_EXT.test(file) || UI_DIR.test(file) || SWIFT_VIEW.test(file);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function uiFilesIn(files: ChangedFile[]): string[] {
|
|
21
|
+
return files
|
|
22
|
+
.filter((f) => f.status !== "D" && isUiPath(f.path))
|
|
23
|
+
.map((f) => f.path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const IMAGE =
|
|
27
|
+
/!\[|<img\b|\.(png|jpe?g|webp|gif)\b|user-images|githubusercontent/i;
|
|
28
|
+
const DESCRIBED =
|
|
29
|
+
/browser check|simulator check|screenshot|chrome-devtools|viewports?\b|\b\d{3,4}\s*[x×]\s*\d{3,4}\b|in (a|the) (real )?browser|in the (ios )?simulator/i;
|
|
30
|
+
|
|
31
|
+
// Lenient on purpose: a nudge, not a gate. Undefined when the body wasn't read.
|
|
32
|
+
export function hasUiEvidence(
|
|
33
|
+
body: string | null | undefined
|
|
34
|
+
): boolean | undefined {
|
|
35
|
+
if (body === null || body === undefined) return undefined;
|
|
36
|
+
return renderedLines(body).some((l) => IMAGE.test(l) || DESCRIBED.test(l));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function uiEvidenceNote(
|
|
40
|
+
files: ChangedFile[],
|
|
41
|
+
evidence: boolean | undefined
|
|
42
|
+
): string | null {
|
|
43
|
+
const ui = uiFilesIn(files);
|
|
44
|
+
if (!ui.length || evidence !== false) return null;
|
|
45
|
+
const shown = ui.slice(0, 3).join(", ");
|
|
46
|
+
const more = ui.length > 3 ? `, +${ui.length - 3} more` : "";
|
|
47
|
+
return `[minor] ${shown}${more}: this changes UI files and the PR body has no browser or simulator evidence (screenshots or a described check). Not a merge gate; add a Browser check section if you checked.`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function withUiEvidenceNote(
|
|
51
|
+
verdict: { status: CheckStatus; detail: string },
|
|
52
|
+
note: string | null
|
|
53
|
+
): { status: CheckStatus; detail: string } {
|
|
54
|
+
return note
|
|
55
|
+
? { status: verdict.status, detail: `${verdict.detail}\n- ${note}`.trim() }
|
|
56
|
+
: verdict;
|
|
57
|
+
}
|
package/lib/tmux/exec.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
+
import { sessionBaseEnv } from "../agents/server-env";
|
|
3
4
|
|
|
4
5
|
const execFileAsync = promisify(execFile);
|
|
5
6
|
|
|
@@ -9,6 +10,7 @@ export async function tmux(args: string[], timeout = 10_000): Promise<string> {
|
|
|
9
10
|
const { stdout } = await execFileAsync("tmux", args, {
|
|
10
11
|
timeout,
|
|
11
12
|
maxBuffer: 8 << 20,
|
|
13
|
+
env: sessionBaseEnv(),
|
|
12
14
|
});
|
|
13
15
|
return stdout;
|
|
14
16
|
}
|
|
@@ -24,7 +26,7 @@ export async function pasteText(
|
|
|
24
26
|
const child = execFile(
|
|
25
27
|
"tmux",
|
|
26
28
|
["load-buffer", "-b", buffer, "-"],
|
|
27
|
-
{ timeout: 10_000 },
|
|
29
|
+
{ timeout: 10_000, env: sessionBaseEnv() },
|
|
28
30
|
(err) => (err ? reject(err) : resolve())
|
|
29
31
|
);
|
|
30
32
|
// tmux gone before it read everything is a failed paste, not a crash.
|